diff --git a/Makefile b/Makefile index 654c777cb..219dc7666 100644 --- a/Makefile +++ b/Makefile @@ -47,10 +47,10 @@ build: $(DOCKER_BASE_OS).build install: $(DOCKER_BASE_OS).install run: - @geodesic + @$(APP_NAME) %.run: %.build %.install - @geodesic + @$(APP_NAME) run/check: @if [[ -n "$$(docker ps --format '{{ .Names }}' --filter name="^/$(APP_NAME)\$$")" ]]; then \ diff --git a/docs/ReleaseNotes-v4.md b/docs/ReleaseNotes-v4.md new file mode 100644 index 000000000..5b139fd84 --- /dev/null +++ b/docs/ReleaseNotes-v4.md @@ -0,0 +1,522 @@ +## Geodesic v4.0.0 Release Notes (draft) + +### Highlights + +#### Better Shell Management + +A much requested feature, Geodesic no longer exits the container when the first shell exits. +Instead, the container runs until all shells have exited. This means you can now run multiple shells +inside the container, and exit them in any order; you no longer have to keep track of which +shell was the first one launched. Unfortunately, this also means that you can no longer +detach and reattach to a shell. + +However, Geodesic now supports another much requested feature: launching a new container +each time you run Geodesic. This is done by setting the `ONE_SHELL` environment variable to "true" +or passing `--one-shell` on the command line. This allows you to run multiple versions of Geodesic, +and also allows you to detach from a shell and reattach to it later. + +Not a new feature, but one that many people were not aware of: you can kill the running +Geodesic container with the command `geodesic stop`. This will stop the container, and it +will be automatically removed (assuming you started it with `geodesic`). Now, however, +there is the possibility you will have several running containers. If this is the case +`geodesic stop` will list the running containers by name. You can then pass the +name as an argument to `geodesic stop` and it will stop that one. + +Another old feature few people knew about: you can have Geodesic automatically +run a command when a shell exits. This was done by creating an executable command named +`geodesic_on_exit` and putting it in your `$PATH`. This feature has been enhanced +in 2 ways: + +1. Now, you can set the name of the command to run when the shell exits via `ON_SHELL_EXIT` + (defaults to `geodesic_on_exit`). Also new: the `ON_SHELL_EXIT` command will have available to it the short ID of the container in which it was running, via the + environment variable `GEODESIC_CONTAINER_EXITING`. +2. You can use the new environment variable `ON_CONTAINER_EXIT` to configure a different + command to run only when the container exits. + +Be aware that the commands are called on a best-effort basis when the Geodesic +launch wrapper exits. If you detach from a shell, the wrapper will run then and +call `ON_SHELL_EXIT`. If you reattach to the shell, the wrapper is not involved, +so quitting the shell or container will not run the cleanup command. + +Alternately, if you quit 2 shells at nearly the same time, the `ON_CONTAINER_EXIT` +command may be called twice. This is because the wrapper does not know which shell +is the last one to exit; it calls the command when the container has stopped +before shell exit processing has finished. + +#### Better Configuration Management + +Geodesic now supports configuration files for customizing the launch of the Geodesic container. +Although Geodesic has for a while been [customizable](https://github.com/cloudposse/geodesic/blob/main/docs/customization.md), +the customization you could configure via files were limited to customizations of the +running Docker container. Previously, customizations regarding how Geodesic is launched were difficult to manage. +Now, you can create a `launch-options.sh` file in the Geodesic configuration directories +to customize the launch of the Geodesic container. The directory search path and the +priority of the files are the same as for the other Geodesic customization files. + +#### Better File System Layout + +Geodesic no longer mounts the entire host user's home directory into the container. +This had been a performance problem and was explicitly discouraged by Docker. Now, +Geodesic mounts only specific directories from the host to the container, and you +have full control over which directories are mounted where (with sensible defaults). + +Inspired by the [Dev Container](https://containers.dev/), the `/localhost` directory +has been removed, and the host's git root directory is mounted to `/workspace` in the container. +This is all configurable, and some configuration for each project will make it easier to +use multiple projects with Geodesic. + +Among other things, this means that your project source directory no longer has to be +under your home directory. You are free to locate it on another drive if you like. + +For reasons lost to history, Geodesic set the container user's home directory to `/conf`. +This caused some problems for people who wanted to run Geodesic as a non-root user. +The `/conf` directory has been removed, and the container user's home directory +(as specified in `/etc/passwd`) is now honored. By default, Geodesic launches as the +`root` user, so the default `$HOME` is `/root`. + +As before, the host user's home directory path is available in the container as +`$LOCAL_HOME`, and mounted files and directories are available at the same paths +in the container as on the host. + +### Breaking Changes + +- Previously, `$HOME` was set to `/conf` in the container. This is no longer the case. + `$HOME` is now set to the shell user's home directory. By default, this is `/root`. + If you launch Geodesic as a non-root user, `$HOME` will be set to that user's home directory, + provided you have properly created the user with `adduser`. By default, the + container user will share configuration with the host user by mounting the host user's + configuration directories into the container user's home directory, allowing + bidirectional updates. + +- The `/conf` directory no longer exists. Generally, what used to be in `/conf` + is now in `/root` if it was created in the Geodesic Dockerfile.debian, or in + `$HOME` (also `/root` by default) if it was created in the Geodesic startup scripts. +- `/conf/.kube/config` has been moved to `/etc/kubeconfig`. It is installed as + `/root/.kube/config`, but this is now expected to be hidden by mounting the + host user's `$HOME/.kube` directory over `/root/.kube`. + +- Previously, if you exited the shell that launched Geodesic, the container would exit, + killing any other running shells. Now, the container will not exit until all shells have exited. + As a side effect, you can no longer reattach to a shell that you have detached from. + You can get something closer to the old behavior by setting `ONE_SHELL=true`. + See [New Default Behavior for Multiple Shells](#new-default-behavior-for-multiple-shells) below + for more details. + +- Previously, the entire host user's home directory was mounted into the container under `/localhost`, + making everything in the host user's home directory available to the container. + Now, only specific directories are mounted, and they are mounted in the container user's + `$HOME` directory. The default directories are `.aws`, `.config`, `.emacs.d`, + `.geodesic`, `.kube`, `.ssh`, and `.terraform.d`. You can add additional directories by setting + the `HOMEDIR_ADDITIONAL_MOUNTS` environment variable. See [The Home Directory](#the-home-directory) below + for more details. + +- Previously, environment variables inside the container could be set in the `~/.geodesic/env` file, + which was passed to Docker via `--env-file`. This file is now ignored. Instead, you should + set environment variables in the customization preferences and overrides. + +- The `/localhost` directory no longer exists. This used to be the single mount point + for the host filesystem, and the host user's entire `$HOME` directory was mounted there. + Now, we no longer mount the entire `$HOME` directory tree into the container. Instead, + we mount specific directories from the host to the container. + - Configuration directories directly under the host user's `$HOME` directory + (such as `.aws` or `.config`) are mounted to the container user's `$HOME` + directory. + - The git repository root directory for the project is mounted to the container's `/workspace` directory. + - Additional directories can be mounted from the host to the container by setting + the `HOST_MOUNTS` environment variable. + + If you were relying on the `/localhost` directory, it would be best to update your scripts to use + either `$HOME`, `$WORKSPACE_MOUNT`, or `$WORKSPACE_FOLDER` as appropriate. As a temporary workaround, + you can run `ln -s "$LOCAL_HOME" /localhost` in you customizations. + +- Previously, you could have Geodesic perform file ownership mapping between host and container + by setting `GEODESIC_HOST_BINDFS_ENABLED=true`; this variable is now deprecated. + Use `MAP_FILE_OWNERSHIP=true` instead. This feature is disabled by default and can + cause issues if enabled unnecessarily, but it is useful if you are having file ownership issues. + See [Files Written to Mounted Host Home Directory Owned by Root User](https://github.com/cloudposse/geodesic/issues/594) + for more details. + +#### Obsolete and Deprecated Features + +##### Custom SSH Support Removed + +When Geodesic was first created, there was no way to share the SSH agent socket between the host and the container. +As a result, Geodesic provided custom SSH support, launching an SSH agent and reading configuration and keys from the host. +Now that Docker supports sharing the SSH agent socket, this custom SSH support is no longer necessary. +If the `SSH_AUTH_SOCK` environment variable is set on the host, it will be used by Docker, and the +Docker container will have access to the SSH agent socket. + +##### Automatic MFA Support Removed + +The `mfa` command and `oathtool` were removed. The `mfa` command was a wrapper around `oathtool` +to generate TOTP codes. It was removed because: + +- We did not have a secure place to store the TOTP key. + - It was being stored in a plaintext file + - It was being stored in `${AWS_DATA_PATH}/${profile}.mfa` which is wrong on several levels: + 1. `$AWS_DATA_PATH` is a `PATH`-like list of directories, not a single directory + 2. `$AWS_DATA_PATH` is meant to direct the AWS SDK to [directories from which to load Python models](https://github.com/boto/botocore/blob/cac78632cabddbc7b64f63d99d419fe16917e09b/botocore/loaders.py#L33), not for storing user data + 3. Actually storing the key in `${AWS_DATA_PATH}/${profile}.mfa` can cause problems for the AWS SDK +- We believe there are better ways to manage MFA, such as 1Password. +- If you still want to use `oathtool`, you can install it yourself. It is very easy to use. + +#### Internal changes less likely to affect users + +- Previously, Geodesic attempted to duplicate host file paths inside the container + using symbolic links. Now Geodesic uses bind mounts instead. This should not affect + the user, but it does require the `SYS_ADMIN` capability. + Geodesic has always run with the `--privileged` flag, which includes `SYS_ADMIN`, so + this only affects people who had removed the `--privileged` flag somehow. + +### New Container File System Layout + +Geodesic v4.0.0 introduces a new file system layout for the Geodesic container, +inspired by the [Dev Container](https://containers.dev/) standard. + +#### The Old Layout + +Previously, the host user's entire home directory was mounted into the container +under `/localhost`. This was done to allow the container to access the host user's +configuration files, such as `.aws` and `.ssh`. However, this had some major drawbacks, +the main one being that Docker had to map all of the user's files and directories into the +container, including, on macOS, Docker's own virtual disk and other dynamic files. +This caused major performance problems in some cases. + +Previously the home directory for the container user was forced to be `/conf`, and files +and directories were linked from `/localhost` to `/conf`. This was done to allow for a single +host mount, back when host mounts were expensive. This was also problematic, as `/conf` was +owned by `root`, and if you wanted to run the Geodesic image as a non-root user, you +had to take extra steps to manage the permissions of `/conf` and its contents. + +#### The New Layout + +##### The Home Directory + +A set of directories are mounted from the user's home directory on the host to the container user's +home directory. These are meant to be directories that contain configuration files that the container's +users will need to access. Project source directories and other directories that are not meant to be +used as configuration directories should not be mounted this way. Mount the project source directories +into the container's workspace instead, and mount other directories via the `HOST_MOUNTS` environment variable, +both of which are described after this section. + +These directories are specified as a comma-separated list of directories (or files) relative to the host user's home directory. +If items in the list are not present on the host, they will be silently ignored. + +- `HOMEDIR_MOUNTS` is a list of directories to mount. It is set by default to `".aws,.config,.emacs.d,.geodesic,.gitconfig,.kube,.ssh,.terraform.d"`. + If you set it to something else, it will replace the default list. Ensure that your Geodesic configuration directory + (default is `$HOME/.config/geodesic`) is mounted. +- `HOMEDIR_ADDITIONAL_MOUNTS` is a list of additional directories to mount. It is appended to the + `HOMEDIR_MOUNTS` list of directories to mount. This allows you to add to the defaults without overriding them. + +Note that you can mount files this way, but it will be difficult to know inside of Geodesic if +the files are on the host or private to the container. When you mount directories, the default +Geodesic prompt will tell you whether the current directory is on the host or not. + +Many files that used to be placed directly in the `/conf` directory can now be placed in subdirectories. +Many applications now support the `XDG Base Directory Specification`, which specifies that configuration +files should be placed in `$XDG_CONFIG_HOME` (defaults to `~/.config/`). This directory is mounted by default. + +- `~/.gitconfig` can be moved to `~/.config/git/config`. +- `~/.bash_profile` can be moved to `~/.bash_profile.d/` and sourced from there. however, we do not recommend this, and we do not + mount `~/.bash_profile.d` by default. Instead, we recommend you put scripts you want to run inside Geodesic in + `~/.config/geodesic/defaults/preferences.d/` where they will be sourced automatically. If you want to share + files between the host and Geodesic, you can use symbolic links, but keep in mind that they must resolve properly in + the container, and the target files must be in a directory that is mounted into the container. You can mount + `~/.bash_profile.d` into the container by setting `HOMEDIR_ADDITIONAL_MOUNTS=".bash_profile.d"`. +- `~/.bashrc` can be moved to `~/.bashrc.d/` and sourced from there. The same caveats apply as for `~/.bash_profile`. +- `~/.emacs` can be moved into its current preferred location, `~/.emacs.d/init.el`. + +##### The Host Mounts + +You can mount any additional directories from the host to the container by setting the `HOST_MOUNTS` environment variable. +This is a comma-separated list of directories to mount, in the format `absolute_host_path[:container_path]`. If the container path is not specified, +it will be the same as the host path. The host path name must be absolute, and `~` is not acceptable. +If you want to place directories under the container user's home directory, use `HOMEDIR_ADDITIONAL_MOUNTS` +as described above. + +Unfortunately, since the colon (`:`) is meaningful to Docker, you cannot mount directories with colons in their names, +and you cannot separate directories with colons. This list must be separated with commas. + +##### The Workspace + +The workspace is where the code on the host lives, and is mounted into the container. +This is controlled by several environment variables, all of which have defaults +settings that can be overridden. + +tl;dr: Either launch Geodesic from the root of your project, or set `WORKSPACE_FOLDER_HOST_DIR` in your `launch-options.sh` file +to the root of your project. (See [Launch Options Files](#launch-options-files) below for details.) +If you do this, you can launch Geodesic from any directory and have the correct directory be the workspace. + +| Variable | Description | +|-----------------------------|-----------------------------------------------------------------------------------------------------------| +| `WORKSPACE_FOLDER_HOST_DIR` | The directory on the host that is the root of the project. | +| `WORKSPACE_MOUNT_HOST_DIR` | The directory on the host that is mounted into the container to make the source code accessible. | +| `WORKSPACE_MOUNT` | The directory in the container where the `WORKSPACE_MOUNT_HOST_DIR` is mounted. Defaults to `/workspace`. | +| `WORKSPACE_FOLDER` | The directory in the container that is considered the root of the project. | + +The variables are set as follows: + +- If you set `WORKSPACE_FOLDER_HOST_DIR` in the environment, that directory will be used as the working directory. It must be an + absolute path: `$HOME/path` is acceptable, `~/path` is not. You can set this in the `launch-options.sh` file for + each image you use, and then you can launch Geodesic from any directory and have the correct directory be the workspace. + If not set, `WORKSPACE_FOLDER_HOST_DIR` defaults to the current working directory, from where Geodesic was launched. + +- If you set `WORKSPACE_MOUNT_HOST_DIR` in the environment, it must be either the same as `WORKSPACE_FOLDER_HOST_DIR` or + a parent of that directory. This directory will be mounted into the container as `WORKSPACE_MOUNT`. If not set: + + - If `WORKSPACE_FOLDER_HOST_DIR` is inside a Git repository, `WORKSPACE_MOUNT_HOST_DIR` will be set to the root of that repository + - If `WORKSPACE_FOLDER_HOST_DIR` is not inside a Git repository, `WORKSPACE_MOUNT_HOST_DIR` will be set to `WORKSPACE_FOLDER_HOST_DIR` + +- Unless explicitly set (not recommended), `WORKSPACE_FOLDER_HOST_DIR`, relative to the parent of `WORKSPACE_MOUNT_HOST_DIR`, will be communicated + to the container as `WORKSPACE_FOLDER` and considered the working directory for the container. +- A symbolic link will be created in the container, so that the host value of `WORKSPACE_FOLDER_HOST_DIR` will + reference the `WORKSPACE_FOLDER`. + + +#### Fixing File Ownership Issues + +Depending on the way you installed Docker, you may have file ownership issues with the files created +from within the container on the host. The default Geodesic user is `root` and if Docker is not translating +file ownership properly, the files will be owned by `root` on the host. This can be fixed by running Docker +in "rootless" mode, but that is not always practical, so Geodesic has special support to handle this case. + +This support used to be enabled by setting `GEODESIC_HOST_BINDFS_ENABLED=true`, but this is now deprecated. +Instead, enable it by setting `MAP_FILE_OWNERSHIP=true`. This will cause Geodesic to use `bindfs` to map the +file ownership between the host and the container. Please note, however, that if Docker is properly translating +file ownership, this setting will cause, rather than fix, file ownership problems, so only use it if needed. +It is disabled by default, because current macOS and best practice Linux Docker installations do not need it. + + +### Note About Command-Line Options + +Geodesic documentation has shown (and for the moment, continues to show) +Geodesic options as settings of shell environment variables. This is because Geodesic +is launched by a `bash` script, and then runs a `bash` shell inside the container. +In this document, we take care to differentiate between options that apply to the +launch script (sometimes referred to as the "wrapper") and options that apply to the +shell inside the container. + +What has always been true, but never clearly spelled out, is that the options that +apply to the launch script can also be set as command-line options. Convert the +environment variable to lower case, optionally replace the `_` with `-`, and prefix +it with `--` and you have the command line option. For example, `ONE_SHELL=false` becomes +`--one-shell=false`. For boolean options, you can leave out the value, so `ONE_SHELL=true` +becomes `--one-shell`. + +To avoid tedious redundancy, we will not usually repeat the command-line options in the +documentation. Instead, we will refer to the environment variable, and you can +convert it to a command-line option as described above. Just remember that they +only apply to launch options, not to configuration of the shell inside the container. + + +### New Default Behavior for Multiple Shells + +Previously, when you launched Geodesic, it would launch a new shell as PID 1. +If you tried to launch Geodesic again, it would not start a new container, but would +instead exec into the container, launching a new shell. This was done to avoid the +overhead of starting a new container each time you wanted a new shell, and has some +advantages and disadvantages with all the shells sharing the same container. +One disadvantage was that if you exited the first shell, the container would exit, +killing any other shells running inside the container. Another disadvantage, +or at least odd behavior, is that if you detached from the first shell, you could +reattach to it later, but if you detached from a shell launched by exec, you could +not reattach to it. Attempting to reattach to it would attach to the first shell, +and you would have 2 terminals sharing the same shell, while the detached shell +would remain abandoned. + +Now, by default, when you launch Geodesic, it launches a tiny init process as PID 1. This init process +monitors the shells running inside the container, and does not exit until all the shells exit. +So now if you quit the first shell while other shells are running, the container will not exit. + +One consequence of this change is that if you detach from any shell, even the first one, you will not be able to +reattach to it. `docker attach` will connect you to the init process, not the shell. So we semi-disable detaching from +the shell by setting an unusual string for the `detachKeys`. + +#### New Option for One Container Per Shell + +An alternative to this new default behavior is to launch a new container each time you run Geodesic. +This is done by setting the `ONE_SHELL` environment variable to "true" in your +`launch-options.sh` file, or using `--one-shell` on the command line. This will cause the wrapper +to launch a new container each time you run it. + +The 2 main advantages of this are: + +1. You can run multiple versions of Geodesic at the same time. This is useful for testing new versions. +2. You can detach from a shell and reattach to it later. + +#### New Options for Cleanup Scripts + +Previously, when the wrapper that launches Geodesic exited, it would run a cleanup script +named `geodesic_on_exit` if it existed. This name was hard coded and not configurable. + +Now, the name of the cleanup script is configurable, and the script makes a distinction between +two events: + +1. ON_SHELL_EXIT: When a shell exits but the container is still running. Defaults to no script. +2. ON_CONTAINER_EXIT: When the container exits. Defaults to `geodesic_on_exit`. + +The caveat here is that these scripts are run when the wrapper exits, not necessarily +when the shell or container exits. This means that if you detach from a shell, the wrapper +will run `$ON_SHELL_EXIT`. If you reattach to the shell, the wrapper is not involved, +so quitting the shell or container will not run the cleanup script. + +### New location for Geodesic configuration files + +Previously, all Geodesic configuration was stored in the `~/.geodesic` directory. +This has been changed to `$XDG_CONFIG_HOME/geodesic` which defaults to `~/.config/geodesic`. +This change was made to follow the +[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). + +If the `$XDG_CONFIG_HOME/geodesic` directory does not exist, Geodesic will +continue to use the `~/.geodesic` directory. If the `$XDG_CONFIG_HOME/geodesic` directory does exist, +the `~/.geodesic` directory will be ignored. + +Previously, environment variables inside the container could be set in the `~/.geodesic/env` file, +which was passed to Docker via `--env-file`. This file is now ignored. Instead, you should +set environment variables in the customization preferences and overrides. + +#### New Customization Options + +As explained in the [Customizing Geodesic](/docs/customization.md) documentation, +there are several ways to customize Geodesic. However, until now, most of these customizations +only applied to customizing the shell inside the Geodesic container. Customizing the +launch of the Geodesic container itself was more difficult. + +##### Launch Options Files + +Geodesic now supports launch options files that customize the launch of the Geodesic container. +Geodesic is launched by a `bash` script and can be customized by setting environment variables. +Using the same directory structure as the Geodesic configuration files, you can create a file +`launch-options.sh` that will be sourced by the script after the defaults are configured but before +they are used. The searched directories depend on the name of the Docker image being launched. +All `launch-options.sh` files are sourced in the order they are found, meaning later ones override earlier ones. +With the configuration directory `$GEODESIC_CONFIG_HOME` (defaults to `$XDG_CONFIG_HOME/geodesic`) and +an image named `ghcr.io/cloudposse/geodesic:4.0.0-debian`, the directories searched, in order, are: + +1. `$GEODESIC_CONFIG_HOME/defaults/` +2. `$GEODESIC_CONFIG_HOME/cloudposse/` +3. `$GEODESIC_CONFIG_HOME/geodesic/` +4. `$GEODESIC_CONFIG_HOME/cloudposse/geodesic/` + +The registry (`ghcr.io/` in the example) is ignored when searching for the `launch-options.sh` file. + + +If the `$GEODESIC_CONFIG_HOME/launch-options.sh` file directly changes the `DOCKER_IMAGE` variable, it will change the +directories being searched in steps 2-4. Later changes, or setting `GEODESIC_IMAGE`, will not change +the directories being searched. + +##### New Customization Command-Line Options + +3 command line options regarding customization have been added: + +1. `--no-custom` (or `--no-customization`, or `--geodesic-customization-disabled`) will disable all user-specific + customizations. This is equivalent to setting `GEODESIC_CUSTOMIZATION_DISABLED=true`. This is useful for + "works in my environment" testing, where you want to disable all customizations to see if the problem is in the + customizations or in the base image. Note that this does not disable changes made by `launch-options.sh`. +2. `--trace` will enable tracing the Geodesic script as it performs customizations. Equivalent to `--trace=custom`. +3. `--trace="custom terminal hist` will enable tracing of the customizations, terminal configuration (mainly with respect + to light and dark mode support), and determining which Bash history file to use, respectively. You can use these options + in any combination, for example, `--trace="hist"`. + +### Dark mode support + +Geodesic's limited color handling had initially assumed terminals are in light mode. +Support for terminals being in dark mode was introduced in Geodesic v2.10.0, +but was not previously well documented. There have also been some enhancements +since then. The following describes the state of support as of v4.0.0. + +#### Switching between light and dark mode + +Geodesic provides basic support for terminal dark and light modes. +Primarily, this is used to ensure Geodesic's colored output is readable in both modes, +for example, black in light mode and white in dark mode. + +There is no standard way to be notified of a terminal's color mode change. Geodesic +listens for SIGWINCH and updates the color mode when receiving it. Some terminals +send this when the color mode changes, but not all do. (For example, macOS Terminal does not.) + +There can be issues with the signal handler. For example, if your computer is +waking from sleep, the signal handler may be called multiple times, but +the terminal may take several seconds to respond to the query about its color mode. +This can result in long delays while Geodesic waits for the terminal to respond, +and if it times out, the response may eventually be written to the terminal +command line, looking something like `10;rgb:0000/0000/000011;rgb:ffff/ffff/ffff`. +This area of Geodesic is still new and under development, so there are likely to be subtle bugs. +If you want to disable this feature, you can set `GEODESIC_TERM_COLOR_AUTO=false`. +If Geodesic detects a problem with the terminal color mode, it will disable this feature +by setting `GEODESIC_TERM_COLOR_AUTO=disabled`. + +You can report issues with this, or any Geodesic feature, via the `#geodesic` +channel in the [Cloud Posse Slack workspace](https://cpco.io/slack?utm_source=github&utm_medium=release_notes&utm_campaign=cloudposse/geodesic&utm_content=slack). + +Geodesic provides a shell function called `update-terminal-color-mode` that can be used to manually +update the terminal mode. This function is called automatically when Geodesic starts, but +if you change the terminal color mode while Geodesic is running, you can call this function +to update the color mode. If your terminal supports calling a function when the color mode changes, +you can call this function from there. Alternately, you can trigger the function call +by resizing the terminal window, which triggers the SIGWINCH signal handler. + +The `update-terminal-color-mode` function takes one argument, which is the terminal color mode, +either `light` or `dark`. If you do not provide an argument, it will attempt to determine +the terminal color mode itself. + +You can query Geodesic for its cached color mode setting by running `get-terminal-color-mode`. + +Changing Geodesic's color mode does not change anything already on the screen. It only affects +future output. + +##### Named text color helpers + +To help you take advantage of the color mode, Geodesic provides a set of named text color helpers. +They are defined as functions that output all their arguments in the named mode. +The named colors are + +- red +- green +- yellow +- cyan + +Note: yellow is problematic. To begin with, "yellow" is not necessarily yellow, +it varies with the terminal theme, and would be better named "caution" or "info". +In addition, it is too light to be used in light mode, so we substitute magenta instead. + +Each of these colors has 4 variations. Using "red" as an example, they would be: + +- red +- bold-red +- red-n +- bold-red-n + +The "bold-color" version outputs text in the bold (or "emphasis") version of the color. + +The "-n" means no newline is output after the text. These versions also include non-printing delimiters around the +non-printing text, making them suitable for use in PS1 prompts. + + +Note that the newline in the plain versions is stripped if run via command substitution, so + +```bash +echo "$(red "Hello") World" +``` + +will not have a newline between "Hello" and "World". + + +The remaining ANSI colors, black, white, blue, and magenta, are not directly provided as named helpers to +discourage their use. They are available via the `_geodesic_color` function, which takes +the same kind of color name as the named helpers as its first argument, and then outputs +the rest of its arguments in that color. For example, + +```bash +_geodesic_color bold-magenta Hello, World +``` + +These colors are not provided as named helpers because they are problematic, and +we want to discourage their use. Nevertheless, you may prefer to use the +`_geodesic_color` function to color text in these colors, because of the +dark mode support. + +- In light mode, yellow is too light to be used, so it is replaced with magenta. + We therefore discourage using magenta as it will not be distinguished from yellow in light mode. +- In dark mode, blue is problematic, so it is replaced with cyan. Also, white and black are swapped. + diff --git a/os/alpine/Dockerfile.alpine b/os/alpine/Dockerfile.alpine index 9609784b3..2121db6c4 100644 --- a/os/alpine/Dockerfile.alpine +++ b/os/alpine/Dockerfile.alpine @@ -241,7 +241,7 @@ RUN helm3 plugin install https://github.com/databus23/helm-diff.git --version v$ # Configure host AWS configuration to be available from inside Docker image # # AWS_DATA_PATH is a PATH-like variable for configuring the AWS botocore library to -# load additional modules. Do not set it. ENV AWS_DATA_PATH=/localhost/.aws +# load additional modules. Do not set it. ARG GEODESIC_AWS_HOME=/localhost/.aws ENV AWS_CONFIG_FILE=${GEODESIC_AWS_HOME}/config ENV AWS_SHARED_CREDENTIALS_FILE=${GEODESIC_AWS_HOME}/credentials diff --git a/os/debian/Dockerfile.debian b/os/debian/Dockerfile.debian index 2394a4ff6..8278601fe 100644 --- a/os/debian/Dockerfile.debian +++ b/os/debian/Dockerfile.debian @@ -98,10 +98,11 @@ RUN for dir in $XDG_DATA_HOME $XDG_CONFIG_HOME $XDG_CACHE_HOME; do \ ENV BANNER "geodesic" ENV MOTD_URL=http://geodesic.sh/motd -ENV HOME=/conf # Install all packages as root USER root +# We used to override user home directory to /conf, but we no longer do that. +ENV HOME=/root # Keep dpkg quiet about running non-interactively RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections @@ -134,8 +135,9 @@ RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages. # Install packages (but only explicitly listed ones) RUN apt-get update && apt-get install -y --no-install-recommends \ $(grep -h -v '^#' /etc/apt/packages.txt /etc/apt/packages-debian.txt | sed -E 's/@(cloudposse|community|testing)//g' ) && \ - mkdir -p /etc/bash_completion.d/ /etc/profile.d/ /conf && \ - touch /conf/.gitconfig + mkdir -p /etc/bash_completion.d/ /etc/profile.d/ # \ + # /conf && \ + # touch /conf/.gitconfig # Install `tofu` as an alternative to `terraform`, if it is available. # Set priority to 5, which is lower than any other Cloud Posse Terraform package, @@ -163,17 +165,15 @@ WORKDIR /tmp COPY --from=python /usr/local/ /usr/local/ # Explicitly set KUBECONFIG to enable kube_ps1 prompt -ENV KUBECONFIG=/conf/.kube/config +ENV KUBECONFIG="${HOME}/.kube/config" # Install an empty kubeconfig to suppress some warnings -COPY rootfs/conf/.kube/config /conf/.kube/config +COPY rootfs/etc/kubeconfig "${KUBECONFIG}" # Set mode on kubeconfig to suppress some warnings while installing tools RUN chmod 600 $KUBECONFIG # # Install kubectl # -# Set KUBERNETES_VERSION and KOPS_BASE_IMAGE in /conf/kops/kops.envrc -# RUN kubectl completion bash > /etc/bash_completion.d/kubectl.sh # https://github.com/ahmetb/kubectx/releases @@ -230,8 +230,8 @@ RUN helm3 plugin install https://github.com/databus23/helm-diff.git --version v$ # Configure host AWS configuration to be available from inside Docker image # # AWS_DATA_PATH is a PATH-like variable for configuring the AWS botocore library to -# load additional modules. Do not set it. ENV AWS_DATA_PATH=/localhost/.aws -ARG GEODESIC_AWS_HOME=/localhost/.aws +# load additional modules. Do not set it. +ARG GEODESIC_AWS_HOME=${HOME}/.aws ENV AWS_CONFIG_FILE=${GEODESIC_AWS_HOME}/config ENV AWS_SHARED_CREDENTIALS_FILE=${GEODESIC_AWS_HOME}/credentials # Region abbreviation types are "fixed" (always 3 chars), "short" (4-5 chars), or "long" (the full AWS string) @@ -330,7 +330,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends pandoc && \ RUN for dir in $XDG_DATA_HOME $XDG_CONFIG_HOME $XDG_CACHE_HOME; do \ chmod -R a+rwX $dir; done -WORKDIR /conf +RUN mkdir /workspace +WORKDIR /workspace ENTRYPOINT ["/bin/bash"] CMD ["-c", "boot"] @@ -338,3 +339,4 @@ CMD ["-c", "boot"] ARG DEV_VERSION ENV GEODESIC_DEV_VERSION=$DEV_VERSION ENV GEODESIC_VERSION="${GEODESIC_VERSION}${GEODESIC_DEV_VERSION:+ (${GEODESIC_DEV_VERSION})}" + diff --git a/os/debian/packages-debian.txt b/os/debian/packages-debian.txt index 774613c4d..7c1f166c2 100644 --- a/os/debian/packages-debian.txt +++ b/os/debian/packages-debian.txt @@ -13,7 +13,6 @@ ldnsutils # locales net-tools netcat-openbsd -oathtool procps wget diff --git a/rootfs/conf/.emacs b/rootfs/conf/.emacs deleted file mode 120000 index 441c4b4b9..000000000 --- a/rootfs/conf/.emacs +++ /dev/null @@ -1 +0,0 @@ -/localhost/.emacs \ No newline at end of file diff --git a/rootfs/conf/.inputrc b/rootfs/conf/.inputrc deleted file mode 120000 index 4671859b3..000000000 --- a/rootfs/conf/.inputrc +++ /dev/null @@ -1 +0,0 @@ -/localhost/.inputrc \ No newline at end of file diff --git a/rootfs/conf/.kube/config b/rootfs/etc/kubeconfig similarity index 79% rename from rootfs/conf/.kube/config rename to rootfs/etc/kubeconfig index 7eb3890ab..6dcc9ec93 100644 --- a/rootfs/conf/.kube/config +++ b/rootfs/etc/kubeconfig @@ -1,2 +1,2 @@ # This is a placeholder Kuberentes configuration file ("kubeconfig") to suppress some warnings. -# We recommend placing real kubeconfig files under /dev/shm to reduce the chance of accidental exposure. \ No newline at end of file +# We recommend placing real kubeconfig files under /dev/shm to reduce the chance of accidental exposure. diff --git a/rootfs/etc/motd b/rootfs/etc/motd deleted file mode 100644 index 8b0a55488..000000000 --- a/rootfs/etc/motd +++ /dev/null @@ -1,11 +0,0 @@ - -IMPORTANT: -# Unless there were errors reported above, -# * Your host $HOME directory should be available under `/localhost` -# * Your host AWS configuration and credentials should be available -# * Use Leapp on your host computer to manage your credentials -# * Leapp is free, open source, and available from https://leapp.cloud -# * Use AWS_PROFILE environment variable to manage your AWS IAM role -# * You can interactively select AWS profiles via the `assume-role` command - - diff --git a/rootfs/etc/motd.sh b/rootfs/etc/motd.sh new file mode 100644 index 000000000..3a21cd10e --- /dev/null +++ b/rootfs/etc/motd.sh @@ -0,0 +1,18 @@ +# This is the local message of the day (motd) that is displayed when you log into the container. +# It is sourced from /etc/profile.d/motd.sh as a bash script so that it can interpolate variables. + +cat << EOF +IMPORTANT: +# Unless there were errors reported above, +# * Configuration from your host \$HOME directory should be available +# * under both \`${LOCAL_HOME}\` and \`${HOME}\`. +# * Your AWS configuration should be available at \`${AWS_CONFIG_FILE}\`. +# * Your host AWS credentials should be available. +# * Use Leapp on your host computer to manage your credentials. +# * Leapp is free, open source, and available from https://leapp.cloud +# * Use the AWS_PROFILE environment variable to manage your AWS IAM role, or +# * you can interactively select AWS profiles via the \`assume-role\` command, +# * which will launch a subshell with your selected profile set. + + +EOF diff --git a/rootfs/etc/profile.d/_01-launch-warning.sh b/rootfs/etc/profile.d/_01-launch-warning.sh new file mode 100644 index 000000000..317f92ba7 --- /dev/null +++ b/rootfs/etc/profile.d/_01-launch-warning.sh @@ -0,0 +1,21 @@ +# Files in the profile.d directory are executed by the lexicographical order of their file names. +# This file is named _01-launch-warning.sh. The leading underscore is needed to ensure this file executes before +# other files with alphabetical names. The number portion is to ensure proper ordering among +# the high-priority scripts. +# +# This file has no dependencies and does not strictly need to come first, +# but it is nice to have the warnings come before other output. + +# In case this output is being piped into a shell, print a warning message + +# Specifically, this guards against: +# docker run -it cloudposse/geodesic:latest-debian | bash + +printf 'printf "\\nIf piping Geodesic output into a shell, do not attach a terminal (-t flag)\\n\\r" >&2; exit 8;' +# In case this output is not being piped into a shell, hide the warning message. +# Use backspaces, because carriage returns may be ignored or translated into newlines. +printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' +printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' +printf ' ' +printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' +printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' diff --git a/rootfs/etc/profile.d/_07-term-mode.sh b/rootfs/etc/profile.d/_07-term-mode.sh index 924c3ff32..c398c4cee 100644 --- a/rootfs/etc/profile.d/_07-term-mode.sh +++ b/rootfs/etc/profile.d/_07-term-mode.sh @@ -20,7 +20,7 @@ # and always returns true. With -l it outputs integer luminance values for foreground # and background colors. With -ll it outputs labels on the luminance values as well. function _is_term_dark_mode() { - local x fg_rgb bg_rgb fg_lum bg_lum + local x fg_rgb bg_rgb fg_lum bg_lum exit_code # Do not try to auto-detect if we are not in a terminal # or if termcap does not think we are in a color terminal @@ -28,18 +28,35 @@ function _is_term_dark_mode() { # Extract the RGB values of the foreground and background colors via OSC 10 and 11. # Redirect output to `/dev/tty` in case we are in a subshell where output is a pipe, # because this output has to go directly to the terminal. + local saved_state=$(stty -g) + [[ $GEODESIC_TRACE =~ "terminal" ]] && echo "$(tput setaf 1)* TERMINAL TRACE: Checking terminal color scheme...$(tput sgr0)" >&2 stty -echo echo -ne '\e]10;?\a\e]11;?\a' >/dev/tty - IFS=: read -t 0.1 -d $'\a' x fg_rgb - IFS=: read -t 0.1 -d $'\a' x bg_rgb - stty echo + # Timeout of 2 was not enough when waking for sleep. + # The second read should be part of the first response, should not need much time at all regardless. + IFS=: read -s -t 1 -d $'\a' x fg_rgb + if [[ $exit_code -gt 128 ]] || [[ -z $fg_rgb ]] && [[ ${GEODESIC_TERM_COLOR_SIGNAL} == "true" ]]; then + IFS=: read -s -t 30 -d $'\a' x fg_rgb + exit_code=$? + [[ $exit_code -gt 128 ]] || [[ -z $fg_rgb ]] && export GEODESIC_TERM_COLOR_AUTO=disabled + fi + [[ $exit_code -gt 128 ]] || exit_code=0 + IFS=: read -s -t 0.5 -d $'\a' x bg_rgb + ((exit_code += $?)) + stty "$saved_state" else if [[ $GEODESIC_TRACE =~ "terminal" ]]; then echo "* TERMINAL TRACE: ${FUNCNAME[0]} called, but not running in a color terminal." >&2 fi fi - if [[ -z $fg_rgb ]] || [[ -z $bg_rgb ]]; then + if [[ ${GEODESIC_TERM_COLOR_SIGNAL} == "true" ]] && [[ ${GEODESIC_TERM_COLOR_AUTO} == "disabled" ]]; then + printf "\n\n\tTerminal light/dark mode detection failed from signal handler. Disabling automatic detection.\n" >&2 + printf "\tYou can manually change modes with\n\n\tupdate-terminal-color-mode [dark|light]\n\n" >&2 + printf "\tYou can re-enable automatic detection with\n\n\tunset GEODESIC_TERM_COLOR_AUTO\n\n" >&2 + fi + + if [[ $exit_code -gt 128 ]] || [[ -z $fg_rgb ]] || [[ -z $bg_rgb ]]; then if [[ $GEODESIC_TRACE =~ "terminal" ]] && tty -s; then echo "$(tput setaf 1)* TERMINAL TRACE: Terminal did not respond to OSC 10 and 11 queries.$(tput sgr0)" >&2 fi diff --git a/rootfs/etc/profile.d/_10-colors.sh b/rootfs/etc/profile.d/_10-colors.sh index a347232b9..ccaa122d5 100755 --- a/rootfs/etc/profile.d/_10-colors.sh +++ b/rootfs/etc/profile.d/_10-colors.sh @@ -10,26 +10,32 @@ # The main change is that it uses the terminal's default colors for foreground and background, # whereas the previous version "reset" the color by setting it to black, which fails in dark mode. -function update-terminal-mode() { +function update-terminal-color-mode() { local new_mode="$1" + local quiet=false case $new_mode in dark | light) ;; + quiet) + quiet=true + # fall through + # shellcheck disable=SC2034 + ;& "") new_mode=$(_is_term_dark_mode -mm) ;; *) - echo "Usage: update-terminal-mode [dark|light]" >&2 + echo "Usage: update-terminal-color-mode [dark|light]" >&2 return 1 ;; esac if [[ $new_mode == "unknown" ]]; then if ! tty -s; then - echo "No terminal detected." >&2 + [[ "$quiet" == "true" ]] || echo "No terminal detected." >&2 elif [[ -z "$(tput op 2>/dev/null)" ]]; then - echo "Terminal does not appear to support color." >&2 + [[ "$quiet" == "true" ]] || echo "Terminal does not appear to support color." >&2 fi new_mode="light" fi @@ -40,10 +46,18 @@ function update-terminal-mode() { [[ "${_geodesic_tput_cache[TERM]}" != "$TERM" ]]; then _geodesic_tput_cache_init "$1" else - echo "Not updating terminal mode from $new_mode to $new_mode" + [[ "$quiet" == "true" ]] || echo "Not updating terminal mode from $new_mode to $new_mode" >&2 fi } +function get-terminal-color-mode() { + echo "${_geodesic_tput_cache[dark_mode]:-light}" +} + +function _is_color_term() { + [[ -t 1 ]] && tty -s && [[ -n "$(tput op 2 2>/dev/null)" ]] +} + # We call `tput` several times for every prompt, and it can add up, so we cache the results. function _geodesic_tput_cache_init() { declare -g -A _geodesic_tput_cache @@ -76,7 +90,7 @@ function _geodesic_tput_cache_init() { # from here, so we need to tell the user to run the command to fix them. if [[ $BASH_SUBSHELL != 0 ]]; then printf "\n* Terminal mode settings have been lost (%s,%s).\n" "$SHLVL" "$BASH_SUBSHELL" >&2 - printf "* Please run: update-terminal-mode \n\n" >&2 + printf "* Please run: update-terminal-color-mode \n\n" >&2 fi local bold=$(tput bold) @@ -108,6 +122,11 @@ function _geodesic_tput_cache_init() { case $new_mode in dark | light) ;; + quiet) + quiet=true + # fall through + # shellcheck disable=SC2034 + ;& "") new_mode=$(_is_term_dark_mode -m) ;; @@ -175,7 +194,7 @@ function _geodesic_color() { # # In bash, the expression `var[subscript]` has, unfortunately, two very different meanings, # depending on whether `var` has been declared as an associative array or not. If `var` has - # NOT been declared an associative arry, then `subscript` is treated as an arithmetic expression. + # NOT been declared an associative array, then `subscript` is treated as an arithmetic expression. # Within an expression, shell variables may be referenced by name without using the parameter # expansion syntax, meaning `subscript` evaluates to `$subscript`, and the value of the variable # `$subscript` is treated as an arithmetic expression (subject to recursive expansion), which is expected @@ -278,3 +297,37 @@ function reset_terminal_colors() { } _geodesic_tput_cache_init + +# Although SIGWINCH is a standard signal to indicate the window *size* has changed, +# some terminals (not sure which ones) also send a SIGWINCH signal when the window colors change. +# For the other terminals, catching SIGWINCH gives users an easy way of triggering a color update: resize the window. +# So we catch the signal to update the terminal colors, preserving any existing signal handlers. + +function _update-terminal-color-mode-sigwinch() { + [[ ${GEODESIC_TERM_COLOR_SIGNAL} == "true" ]] || [[ ${GEODESIC_TERM_COLOR_AUTO:-true} != "true" ]] && return 0 + + # Ignore repeated signals while a signal is being processed + export GEODESIC_TERM_COLOR_SIGNAL=true + update-terminal-color-mode quiet + unset GEODESIC_TERM_COLOR_SIGNAL +} + +if _is_color_term; then + # We install the trap handler whether GEODESIC_TERM_COLOR_AUTO is set or not, + # because we will not be able to detect the change in that variable if + # it started out disabled and then someone enables it. + + # Save existing trap (if any) + existing_trap=$(trap -p WINCH) + + # Set up new trap that runs both the existing trap and the update-terminal-color-mode function + if [ -n "$existing_trap" ]; then + # Extract the existing command from the trap output + existing_cmd=$(echo "$existing_trap" | sed "s/trap -- '\(.*\)' SIGWINCH/\1/") + trap "${existing_cmd}; _update-terminal-color-mode-sigwinch" WINCH + else + trap _update-terminal-color-mode-sigwinch WINCH + fi + + unset existing_trap existing_cmd +fi diff --git a/rootfs/etc/profile.d/_20-localhost.sh b/rootfs/etc/profile.d/_20-localhost.sh deleted file mode 100644 index bea2734d5..000000000 --- a/rootfs/etc/profile.d/_20-localhost.sh +++ /dev/null @@ -1,31 +0,0 @@ -# Files in the profile.d directory are executed by the lexicographical order of their file names. -# This file is named _20-localhost.sh. The leading underscore is needed to ensure this file executes before -# other files that depend on the file system mapping defined here. -# The number portion is to ensure proper ordering among the high-priority scripts. -# This file has only depends on colors.sh and should come before any scripts that -# attempt to access files on the host via `/localhost`. - -if [[ $SHLVL == 1 ]] && [[ -n $GEODESIC_HOST_UID ]] && [[ -n $GEODESIC_HOST_GID ]] && - [[ -n $GEODESIC_LOCALHOST ]] && df -a | grep -q " ${GEODESIC_LOCALHOST}\$"; then - # bindfs on Alpine does not support the `-o nonempty` option - [[ $GEODESIC_OS == "alpine" ]] || o_nonempty="-o nonempty" - if [[ $(df -a | grep ' /localhost$' | cut -f1 -d' ') == ${GEODESIC_LOCALHOST} ]]; then - echo "# Host file ownership mapping already configured" - export GEODESIC_LOCALHOST_MAPPED_DEVICE="${GEODESIC_LOCALHOST}" - elif df -a | grep -q ' /localhost$'; then - red "# Host filesystems found mounted at both /localhost and /localhost.bindfs." - red "# * Verify that content under /localhost is what you expect." - red "# * Report the issue at https://github.com/cloudposse/geodesic/issues" - red "# * Include the output of \`env | grep GEODESIC\` and \`df -a\` in your issue description." - elif bindfs $o_nonempty ${GEODESIC_BINDFS_OPTIONS} "--map=${GEODESIC_HOST_UID}/0:@${GEODESIC_HOST_GID}/@0" "${GEODESIC_LOCALHOST}" /localhost; then - green "# BindFS mapping of ${GEODESIC_LOCALHOST} to /localhost enabled." - green "# Files created under /localhost will have UID:GID ${GEODESIC_HOST_UID}:${GEODESIC_HOST_GID} on host." - export GEODESIC_LOCALHOST_MAPPED_DEVICE="${GEODESIC_LOCALHOST}" - else - red "# ERROR: Unable to mirror /localhost.bindfs to /localhost" - red "# * Report the issue at https://github.com/cloudposse/geodesic/issues" - red "# * Work around the issue by unsetting shell environment variable GEODESIC_HOST_BINDFS_ENABLED." - red "# * Exiting." - exec false - fi -fi diff --git a/rootfs/etc/profile.d/_20-mounts.sh b/rootfs/etc/profile.d/_20-mounts.sh new file mode 100644 index 000000000..a04236cc1 --- /dev/null +++ b/rootfs/etc/profile.d/_20-mounts.sh @@ -0,0 +1,132 @@ +# Files in the profile.d directory are executed by the lexicographical order of their file names. +# This file is named _20-mounts.sh. The leading underscore is needed to ensure this file executes before +# other files that depend on the file system mapping defined here. +# The number portion is to ensure proper ordering among the high-priority scripts. +# This file has only depends on colors.sh and should come before any scripts that +# attempt to access files on the host. + +# We only need to run this once, so we check the shell level to avoid running it in subshells. +# Still, we can run multiple shells, so it has to be idempotent. +function _map_mounts() { + if ! [[ -d "${HOME}" ]]; then + red "# ERROR: HOME directory ${HOME} does not exist. Fatal error." + return 9 + fi + + export GEODESIC_HOST_PATHS=() + local bindfs_opts=(-o nonempty ${GEODESIC_BINDFS_OPTIONS}) + if [[ "$MAP_FILE_OWNERSHIP" == "true" ]]; then + GEODESIC_HOST_PATHS+=("/.BINDFS/") + if [[ -z $GEODESIC_HOST_UID ]] || [[ -z $GEODESIC_HOST_GID ]]; then + red '# ERROR: `$MAP_FILE_OWNERSHIP` is set to "true" but `$GEODESIC_HOST_UID` and `$GEODESIC_HOST_GID` are not set.' + red '# File ownership mapping will not be enabled.' + else + green "# File ownership mapping enabled." + green "# Files created on host will have UID:GID ${GEODESIC_HOST_UID}:${GEODESIC_HOST_GID} on host." + bindfs_opts+=("--map=${GEODESIC_HOST_UID}/0:@${GEODESIC_HOST_GID}/@0") + fi + fi + + function _ensure_dest() { + local src="$1" + local dest="$2" + local type + + if [[ "${src}" -ef "${dest}" ]]; then + type="same" + elif [[ -L "${src}" ]]; then + red "# ERROR: Supposedly mounted '${src}' is a symlink. Skipping." + type="symlink" + elif [[ -d "${src}" ]]; then + mkdir -p "${dest}" + type="dir" + elif [[ -f "${src}" ]]; then + if ! [[ -f "${dest}" ]]; then + mkdir -p "$(dirname "${dest}")" + touch "${dest}" + fi + type="file" + else + red "# ERROR: Supposedly mounted '${src}' does not exist. Skipping." + type="missing" + fi + echo "${type}" + } + + function _map_owner() { + [[ "$MAP_FILE_OWNERSHIP" == "true" ]] || return 0 + local dest="$1" + local src="/.BINDFS${dest}" + + local type="$(_ensure_dest "${src}" "${dest}")" + if [[ "$type" == "dir" ]] || [[ "$type" = "file" ]]; then + findmnt -fn "${dest}" >/dev/null || + bindfs "${bindfs_opts[@]}" "${src}" "${dest}" + fi + } + + function _map_host() { + local src="$1" + local dest="$2" + local type="$(_ensure_dest "${src}" "${dest}")" + + [[ $type == "dir" ]] && GEODESIC_HOST_PATHS+=("${src}/" "${dest}/") + + if [[ "$type" == "dir" ]] || [[ "$type" = "file" ]]; then + findmnt -fn "${dest}" >/dev/null || + mount --bind "${src}" "${dest}" + fi + } + + # Host mounts are already mounted at the desired path, no need to alias them + IFS='|' read -ra paths <<<"${GEODESIC_HOST_MOUNTS}" + for p in "${paths[@]}"; do + _map_owner "$p" + [[ -d "$p" ]] && GEODESIC_HOST_PATHS+=("${p}/") + done + + # Map the workspace mount + if [[ -z "${WORKSPACE_MOUNT_HOST_DIR}" ]] || [[ "${WORKSPACE_MOUNT_HOST_DIR}" == "${WORKSPACE_MOUNT}" ]]; then + WORKSPACE_MOUNT_HOST_DIR="${WORKSPACE_MOUNT}" + yellow "# No host mapping found for Workspace." + else + _map_owner "${WORKSPACE_MOUNT_HOST_DIR}" + _map_host "${WORKSPACE_MOUNT_HOST_DIR}" "${WORKSPACE_MOUNT}" + fi + + # Map the home directory subdirectories + + # although we call it "dirs", it can be files too + local dirs + IFS='|' read -ra dirs <<<"${GEODESIC_HOMEDIR_MOUNTS}" + if ((${#dirs[@]} == 0)); then + yellow "# No host user home directories to map to container user home." + return 0 + fi + + if [[ -z "${LOCAL_HOME}" ]]; then + red "# ERROR: LOCAL_HOME is not set. Cannot map host user's home to container user's home." + return 0 + fi + + # Set up file ownership mapping for the LOCAL_HOME directory + for d in "${dirs[@]}"; do + _map_owner "${LOCAL_HOME}/$d" + done + + if [[ "${LOCAL_HOME}" == "${HOME}" ]]; then + yellow "# LOCAL_HOME is the same as HOME. No need to map directories to ." + return 0 + fi + + # Map the LOCAL_HOME directory to the HOME directory + for d in "${dirs[@]}"; do + _map_host "${LOCAL_HOME}/$d" "${HOME}/$d" + done +} + +if [[ $SHLVL == 1 ]]; then + _map_mounts +fi + +unset -f _map_mounts _map_owner _map_host _ensure_dest diff --git a/rootfs/etc/profile.d/_30-geodesic-config.sh b/rootfs/etc/profile.d/_30-geodesic-config.sh index 96b44ce41..7fa18c8b8 100755 --- a/rootfs/etc/profile.d/_30-geodesic-config.sh +++ b/rootfs/etc/profile.d/_30-geodesic-config.sh @@ -16,7 +16,7 @@ # * If it is a directory, all the non-hidden files in that directory are loaded in glob sort order # # Several directories are searched for resources, in this order: -# * $base/defaults/ ($base itself defaults to /localhost/.geodesic, can be set via GEODESIC_CONFIG_HOME) +# * $base/defaults/ ($base itself defaults to ${HOME}/.config/geodesic, can be set via GEODESIC_CONFIG_HOME) # * $base/ if and only if there is no $base/defaults/ directory # * $base/$(dirname $DOCKER_IMAGE)/defaults/ # * $base/$(dirname $DOCKER_IMAGE)/ if and only if there is no $base/$(dirname $DOCKER_IMAGE)/defaults/ directory @@ -88,7 +88,7 @@ function _expand_dir_or_file() { local -n expand_list=$1 local resource=$2 local dir=${3-${PWD}} - local default_exclusion_pattern="(~|.bak|.log|.old|.orig|.txt|.md|.disabled)$" + local default_exclusion_pattern="(~|.bak|.log|.old|.orig|.txt|.md|.disabled|#)$" local exclude="${GEODESIC_AUTO_LOAD_EXCLUSIONS:-$default_exclusion_pattern}" [[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: LOOKING for resources of type "$resource" in "$dir" diff --git a/rootfs/etc/profile.d/_40-preferences.sh b/rootfs/etc/profile.d/_40-preferences.sh index 9dcf66074..a7cca37bb 100755 --- a/rootfs/etc/profile.d/_40-preferences.sh +++ b/rootfs/etc/profile.d/_40-preferences.sh @@ -5,17 +5,6 @@ # This file depends on colors.sh, geodesic-config.sh, and localhost.sh and should come after them. # This file loads user preferences/customizations and must load before any user-visible configuration takes place. -# In case this output is being piped into a shell, print a warning message -# Specifically, this guards against: -# docker run -it cloudposse/geodesic:latest-debian | bash -printf 'printf "\\nIf piping Geodesic output into a shell, do not attach a terminal (-t flag)\\n" >&2; exit 8;' -# In case this output is not being piped into a shell, hide the warning message -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' -printf ' ' -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' -printf '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' - # Parse the GEODESIC_TRACE variable and set the internal _GEODESIC_TRACE_CUSTOMIZATION flag if needed if [[ $GEODESIC_TRACE =~ custom ]]; then export _GEODESIC_TRACE_CUSTOMIZATION=true @@ -25,54 +14,32 @@ fi [[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: GEODESIC_CONFIG_HOME is found to be "${GEODESIC_CONFIG_HOME:-}" -# If LOCAL_HOME is set, create a symbolic link so host pathnames (at least the ones under $HOME) work inside the shell -if [[ -n $LOCAL_HOME && ! -e $LOCAL_HOME ]]; then - mkdir -p $(dirname "${LOCAL_HOME}") && ln -s /localhost "${LOCAL_HOME}" || - echo $(red Unable to create symbolic link $LOCAL_HOME '->' /localhost) - [[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: linked $LOCAL_HOME '->' /localhost -fi - # # Determine the base directory for all customizations. # We do some extra processing because GEODESIC_CONFIG_HOME needs to be set as a path in the Geodesic file system, # but the user may have set it as a path on the host computer system. We try to accomodate that by # searching a few other places for the directory if $GEODESIC_CONFIG_HOME does point to a valid directory export GEODESIC_CONFIG_HOME -_GEODESIC_CONFIG_HOME_DEFAULT="/localhost/.geodesic" +_GEODESIC_CONFIG_HOME_DEFAULT="/root/.config/geodesic" if [[ -z $GEODESIC_CONFIG_HOME ]]; then # Not set, use default GEODESIC_CONFIG_HOME="${_GEODESIC_CONFIG_HOME_DEFAULT}" elif [[ ! -d $GEODESIC_CONFIG_HOME ]]; then - # Set, but not correctly. See if it is relative to /localhost (host ~) - if [[ -d /localhost/$GEODESIC_CONFIG_HOME ]]; then - GEODESIC_CONFIG_HOME="/localhost/$GEODESIC_CONFIG_HOME" - # See if it is a full host path ending under host ~ - elif [[ -d /localhost/$(basename $GEODESIC_CONFIG_HOME) ]]; then - GEODESIC_CONFIG_HOME="/localhost/$(basename $GEODESIC_CONFIG_HOME)" + if [[ -n $KUBERNETES_PORT ]]; then + green "# Kubernetes host detected, Geodesic customization disabled." + export GEODESIC_CUSTOMIZATION_DISABLED="No config dir and Kubernetes detected" else - echo $(red Invalid value of GEODESIC_CONFIG_HOME: "${GEODESIC_CONFIG_HOME}") - echo $(red GEODESIC_CONFIG_HOME should be relative to /localhost \(normally your home directory\)) - echo $(red Using default value of ${_GEODESIC_CONFIG_HOME_DEFAULT} instead) - GEODESIC_CONFIG_HOME="${_GEODESIC_CONFIG_HOME_DEFAULT}" + red "# GEODESIC_CONFIG_HOME is set to a non-existent directory: ${GEODESIC_CONFIG_HOME}" >&2 + red "# No Geodesic configuration will be loaded." >&2 fi fi -if [[ ! -d $GEODESIC_CONFIG_HOME ]]; then - if ! df -a | grep -q " ${GEODESIC_LOCALHOST:-/localhost}\$"; then - if [[ -n $KUBERNETES_PORT ]]; then - echo $(green Kubernetes host detected, Geodesic customization disabled.) - else - red "########################################################################################" >&2 - red "# No filesystem is mounted at $(bold ${GEODESIC_LOCALHOST:-/localhost}) which limits Geodesic functionality." >&2 - boot install - fi - export GEODESIC_CUSTOMIZATION_DISABLED="/localhost not a volume" - elif mkdir -p $GEODESIC_CONFIG_HOME; then - echo $(yellow Created directory "$GEODESIC_CONFIG_HOME" '(GEODESIC_CONFIG_HOME)') - else - echo $(red Cannot create directory "$GEODESIC_CONFIG_HOME" '(GEODESIC_CONFIG_HOME)') - fi +[[ -n ${WORKSPACE_MOUNT} ]] || export WORKSPACE_MOUNT=/workspace +if [[ -z $(find "${WORKSPACE_MOUNT}" -mindepth 1 -maxdepth 1) ]]; then + red "########################################################################################" >&2 + red "# No filesystem is mounted at $(bold ${WORKSPACE_MOUNT}) which limits Geodesic functionality." >&2 + boot install fi unset _GEODESIC_CONFIG_HOME_DEFAULT @@ -114,13 +81,8 @@ function _geodesic_set_histfile() { local histfile_list=(${HISTFILE:-${GEODESIC_CONFIG_HOME}/history}) _search_geodesic_dirs histfile_list history export HISTFILE="${histfile_list[-1]}" - if [[ ! $HISTFILE =~ ^/localhost/ ]]; then - echo "* $(yellow Not allowing \"HISTFILE=${HISTFILE}\".)" - mkdir -p "${GEODESIC_CONFIG_HOME}/${DOCKER_IMAGE}/" && HISTFILE="${GEODESIC_CONFIG_HOME}/${DOCKER_IMAGE}/history" && - touch "$HISTFILE" || HISTFILE="${GEODESIC_CONFIG_HOME}/history" - echo "* $(yellow HISTFILE forced to \"${HISTFILE}\".)" - fi - [[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo trace: HISTFILE set to "${HISTFILE}" + [[ -n $HISTFILE ]] || HISTFILE="${HOME}/.bash_history" + [[ -n $_GEODESIC_TRACE_CUSTOMIZATION ]] && echo 'trace: HISTFILE set to "'"${HISTFILE}"'"' } _geodesic_set_histfile diff --git a/rootfs/etc/profile.d/_50-workdir.sh b/rootfs/etc/profile.d/_50-workdir.sh deleted file mode 100644 index 64f9c773e..000000000 --- a/rootfs/etc/profile.d/_50-workdir.sh +++ /dev/null @@ -1,73 +0,0 @@ -# Files in the profile.d directory are executed by the lexicographical order of their file names. -# This file is named _50-workdir.sh. The leading underscore is needed to ensure this file -# executes before other files that may depend on it. -# The number portion is to ensure proper ordering among the high-priority scripts. -# This file depends on colors.sh, localhost.sh, and preferences,sh and must come after them -# - -# Outputs the device the file resides on, or /dev/null if the file does not exist -function _file_device() { - { [[ -e $1 ]] && df --output=source "$1" | tail -1; } || echo '/dev/null' -} - -# file_on_host is true when the argument is a file or directory that appears to be on the Host file system. -# Intended to support files on user-defined bind mounts in addition to `/localhost`. -# This function is run by the command line prompt setup, so it should be very fast. -# Therefore we cache some info in the environment. -if [[ $GEODESIC_LOCALHOST_DEVICE == "disabled" ]]; then - red "# Host filesystem device detection disabled." -elif df -a | grep -q " ${GEODESIC_LOCALHOST:-/localhost}\$"; then - export GEODESIC_LOCALHOST_DEVICE=$(_file_device "${GEODESIC_LOCALHOST:-/localhost}") - if [[ $GEODESIC_LOCALHOST_DEVICE == $(_file_device /) ]]; then - red "# Host filesystem device detection failed. Falling back to \"path starts with /localhost\"." - GEODESIC_LOCALHOST_DEVICE="same-as-root" - fi -else - export GEODESIC_LOCALHOST_DEVICE="missing" -fi - -function file_on_host() { - if [[ $GEODESIC_LOCALHOST_DEVICE =~ ^(disabled|missing)$ ]]; then - return 1 - elif [[ $GEODESIC_LOCALHOST_DEVICE == "same-as-root" ]]; then - [[ $(readlink -e "$1") =~ ^/localhost ]] - else - local dev="$(_file_device "$1")" - [[ $dev == $GEODESIC_LOCALHOST_DEVICE ]] || [[ $dev == $GEODESIC_LOCALHOST_MAPPED_DEVICE ]] - fi -} - -function _default_initial_wd() { - if [[ -d /stacks ]]; then - # Newer default using `atmos` and stacks - export GEODESIC_WORKDIR="/" - else - # Older default working directory - export GEODESIC_WORKDIR="/conf" - fi - red "# Defaulting initial working directory to \"${GEODESIC_WORKDIR}\"" -} - -# You can set GEODESIC_WORKDIR in your Geodesic preferences to have full control of your starting working directory -if [[ -d $GEODESIC_WORKDIR ]]; then - [[ $SHLVL == 1 ]] && green "# Initial working directory configured as ${GEODESIC_WORKDIR}" -else - if [[ -d $GEODESIC_HOST_CWD ]]; then - if [[ -n $LOCAL_HOME ]] && { [[ $GEODESIC_LOCALHOST_DEVICE == "disabled" ]] || file_on_host "$GEODESIC_HOST_CWD"; }; then - export GEODESIC_WORKDIR=$(readlink -e "${GEODESIC_HOST_CWD}") - green "# Initial working directory set from host CWD to ${GEODESIC_WORKDIR}" - else - red "# Host CWD \"${GEODESIC_HOST_CWD}\" does not appear to be accessible from this container" - _default_initial_wd - fi - else - red "# No configured working directory is accessible:" - red "# GEODESIC_WORKDIR is \"$GEODESIC_WORKDIR\"" - red "# GEODESIC_HOST_CWD is \"$GEODESIC_HOST_CWD\"" - _default_initial_wd - fi -fi - -[[ $SHLVL == 1 ]] && cd "${GEODESIC_WORKDIR}" - -unset -f _default_initial_wd diff --git a/rootfs/etc/profile.d/_50-workspace.sh b/rootfs/etc/profile.d/_50-workspace.sh new file mode 100644 index 000000000..413475162 --- /dev/null +++ b/rootfs/etc/profile.d/_50-workspace.sh @@ -0,0 +1,28 @@ +# Files in the profile.d directory are executed by the lexicographical order of their file names. +# This file is named _50-workdir.sh. The leading underscore is needed to ensure this file +# executes before other files that may depend on it. +# The number portion is to ensure proper ordering among the high-priority scripts. +# This file depends on colors.sh, localhost.sh, and preferences.sh and must come after them +# + +# file_on_host is true when the argument is a file or directory that appears to be on the Host file system. + +function file_on_host() { + local dir="$(readlink -e "$1")" + local path + for path in "${GEODESIC_HOST_PATHS[@]}"; do + if [[ "$path" == "${dir}/" || "$dir" == "$path"* ]]; then + return 0 + fi + done + return 1 +} + +if [[ $SHLVL == 1 ]]; then + if [[ -d ${WORKSPACE_FOLDER:=${WORKSPACE_MOUNT}} ]]; then + green "# Initial working directory configured as ${WORKSPACE_FOLDER}" + cd "${WORKSPACE_FOLDER}" + else + red "# Configured work directory ${WORKSPACE_FOLDER} does not appear to be accessible from this container" + fi +fi diff --git a/rootfs/etc/profile.d/atmos.sh b/rootfs/etc/profile.d/atmos.sh index 9e924fd6e..5891215f6 100644 --- a/rootfs/etc/profile.d/atmos.sh +++ b/rootfs/etc/profile.d/atmos.sh @@ -9,21 +9,21 @@ function atmos_configure_base_path() { return fi - # If $GEODESIC_WORKDIR contains both a "stacks" and "components" directory, + # If $WORKSPACE_FOLDER contains both a "stacks" and "components" directory, # use it as the $ATMOS_BASE_PATH - if [[ -d "${GEODESIC_WORKDIR}/stacks" ]] && [[ -d "${GEODESIC_WORKDIR}/components" ]]; then - export ATMOS_BASE_PATH="${GEODESIC_WORKDIR}" - green "# Setting ATMOS_BASE_PATH to \"$ATMOS_BASE_PATH\" based on children of workdir" + if [[ -d "${WORKSPACE_FOLDER}/stacks" ]] && [[ -d "${WORKSPACE_FOLDER}/components" ]]; then + export ATMOS_BASE_PATH="${WORKSPACE_FOLDER}" + green "# Setting ATMOS_BASE_PATH to \"$ATMOS_BASE_PATH\" based on children of workspace folder" return fi - # If $GEODESIC_WORKDIR is a descendent of either a "stacks" or "components" directory, + # If $WORKSPACE_FOLDER is a descendent of either a "stacks" or "components" directory, # use the parent of that directory as ATMOS_BASE_PATH - if [[ "${GEODESIC_WORKDIR}" =~ /(stacks|components)/ ]]; then - if [[ "${GEODESIC_WORKDIR}" =~ /stacks/ ]]; then - export ATMOS_BASE_PATH="${GEODESIC_WORKDIR%/stacks/*}" + if [[ "${WORKSPACE_FOLDER}" =~ /(stacks|components)/ ]]; then + if [[ "${WORKSPACE_FOLDER}" =~ /stacks/ ]]; then + export ATMOS_BASE_PATH="${WORKSPACE_FOLDER%/stacks/*}" else - export ATMOS_BASE_PATH="${GEODESIC_WORKDIR%/components/*}" + export ATMOS_BASE_PATH="${WORKSPACE_FOLDER%/components/*}" fi green "# Setting ATMOS_BASE_PATH to \"$ATMOS_BASE_PATH\" based on parent of workdir" return diff --git a/rootfs/etc/profile.d/aws.sh b/rootfs/etc/profile.d/aws.sh index 642801a16..ae6459f81 100755 --- a/rootfs/etc/profile.d/aws.sh +++ b/rootfs/etc/profile.d/aws.sh @@ -2,33 +2,47 @@ export AWS_REGION_ABBREVIATION_TYPE=${AWS_REGION_ABBREVIATION_TYPE:-fixed} export AWS_DEFAULT_SHORT_REGION=${AWS_DEFAULT_SHORT_REGION:-$(aws-region --${AWS_REGION_ABBREVIATION_TYPE} ${AWS_DEFAULT_REGION:-us-west-2})} -export GEODESIC_AWS_HOME="${GEODESIC_AWS_HOME:-/localhost/.aws}" +export GEODESIC_AWS_HOME + +function _aws_config_home() { + for dir in "${GEODESIC_AWS_HOME}" "${LOCAL_HOME}/.aws" "${HOME}/.aws"; do + if [ -d "${dir}" ]; then + GEODESIC_AWS_HOME="${dir}" + break + fi + done + + if [ -z "${GEODESIC_AWS_HOME}" ]; then + yellow "# No AWS configuration directory found, using ${HOME}/.aws" + GEODESIC_AWS_HOME="${HOME}/.aws" + fi -# `aws configure` does not respect ENVs -if [ ! -e "${HOME}/.aws" ]; then - # -e fails if the target is a link to a non-existent file, remove dead link if it exists - [ -L "${HOME}/.aws" ] && rm -f "${HOME}/.aws" if [ ! -d "${GEODESIC_AWS_HOME}" ]; then - if mkdir ${GEODESIC_AWS_HOME}; then # allow error message to be printed - ln -s "${GEODESIC_AWS_HOME}" "${HOME}/.aws" - else + if ! mkdir "${GEODESIC_AWS_HOME}"; then # allow error message to be printed + local first_try="${GEODESIC_AWS_HOME}" export GEODESIC_AWS_HOME="${HOME}/.aws" - mkdir ${GEODESIC_AWS_HOME} - if [ -n "${AWS_CONFIG_FILE}" ] && [ ! -f "${AWS_CONFIG_FILE}" ]; then - AWS_CONFIG_FILE="${GEODESIC_AWS_HOME}/config" + if mkdir "${GEODESIC_AWS_HOME}"; then + if [ -n "${AWS_CONFIG_FILE}" ] && [ ! -f "${AWS_CONFIG_FILE}" ]; then + AWS_CONFIG_FILE="${GEODESIC_AWS_HOME}/config" + fi + else + red "# Could not use ${first_try}, or ${GEODESIC_AWS_HOME} for AWS configuration, giving up." + return 1 fi fi - chmod 700 ${GEODESIC_AWS_HOME} + chmod 700 "${GEODESIC_AWS_HOME}" fi - ln -s "${GEODESIC_AWS_HOME}" "${HOME}/.aws" -fi -if [ ! -f "${AWS_CONFIG_FILE:=${GEODESIC_AWS_HOME}/config}" ] && [ -d ${GEODESIC_AWS_HOME} ]; then - echo "# Initializing ${AWS_CONFIG_FILE}" - # Required for AWS_PROFILE=default - echo '[default]' >"${AWS_CONFIG_FILE}" - chmod 600 "${AWS_CONFIG_FILE}" -fi + if [ ! -f "${AWS_CONFIG_FILE:=${GEODESIC_AWS_HOME}/config}" ] && [ -d "${GEODESIC_AWS_HOME}" ]; then + echo "# Initializing ${AWS_CONFIG_FILE}" + # Required for AWS_PROFILE=default + echo '[default]' >"${AWS_CONFIG_FILE}" + chmod 600 "${AWS_CONFIG_FILE}" + fi +} + +_aws_config_home +unset -f _aws_config_home # Install autocompletion rules for aws CLI v1 and v2 for __aws in aws aws1 aws2; do @@ -107,8 +121,26 @@ function export_current_aws_role() { if [[ -n $profile_target ]]; then profile_arn=$(aws --profile "${profile_target}" sts get-caller-identity --output text --query 'Arn' 2>/dev/null | cut -d/ -f1-2) if [[ $profile_arn == $current_role ]]; then - export ASSUME_ROLE="$profile_target" - return + if [[ $profile_target == "default" ]] || [[ $profile_target =~ -identity$ ]]; then + # Make some effort to find a better name for the role, but only check the config file, not credentials. + local config_file="${AWS_CONFIG_FILE:-\~/.aws/config}" + if [[ -r $config_file ]]; then + # Assumed roles in AWS config file use the role ARN, not the assumed role ARN, so adjust accordingly. + local role_arn=$(printf "%s" "$current_role" | sed 's/:sts:/:iam:/g' | sed 's,:assumed-role/,:role/,') + role_name=($(crudini --get --format=lines "$config_file" | grep "$role_arn" | cut -d' ' -f 3)) + for rn in "${role_name[@]}"; do + if [[ $rn == "default" ]] || [[ $rn =~ *-identity$ ]]; then + continue + else + export ASSUME_ROLE=$rn + return + fi + done + fi + else + export ASSUME_ROLE="$profile_target" + return + fi fi echo "* $(red Profile is set to $profile_target but current role does not match:)" echo "* $(red $current_role)" diff --git a/rootfs/etc/profile.d/banner.sh b/rootfs/etc/profile.d/banner.sh index a424774c1..31cda35e8 100755 --- a/rootfs/etc/profile.d/banner.sh +++ b/rootfs/etc/profile.d/banner.sh @@ -1,10 +1,3 @@ -COLOR_RESET="" -BANNER_COMMAND="${BANNER_COMMAND:-figurine}" -BANNER_COLOR="${BANNER_COLOR:-}" -BANNER_INDENT="${BANNER_INDENT:- }" -# See font examples at http://www.figlet.org/examples.html -BANNER_FONT="${BANNER_FONT:-Nancyj.flf}" # " IDE parser fix - if [ "${SHLVL}" == "1" ]; then function _check_support() { if grep -qsE 'GenuineIntel|AuthenticAMD' /proc/cpuinfo; then @@ -28,6 +21,15 @@ if [ "${SHLVL}" == "1" ]; then } function _header() { + local ESC=$'\e' + local CYAN="${ESC}[36m" + local COLOR_RESET # Have to be careful because of dark mode + local BANNER_COMMAND="${BANNER_COMMAND:-figurine}" + local BANNER_COLOR="${BANNER_COLOR:-${CYAN}}" + local BANNER_INDENT="${BANNER_INDENT:- }" + # See font examples at http://www.figlet.org/examples.html + local BANNER_FONT="${BANNER_FONT:-Nancyj.flf}" + local vstring local debian_version="/etc/debian_version" @@ -43,9 +45,11 @@ if [ "${SHLVL}" == "1" ]; then fi if [ -n "${BANNER}" ]; then if [ "$BANNER_COMMAND" == "figlet" ]; then + local color_off="$(tput op 2>/dev/null)" # reset foreground and background colors to defaults + tty -s && [[ -n "$color_off " ]] || BANNER_COLOR="" echo "${BANNER_COLOR}" ${BANNER_COMMAND} -w 200 "${BANNER}" | sed "s/^/${BANNER_INDENT}/" - echo "${COLOR_RESET}" + echo "${color_off}" elif [ "$BANNER_COMMAND" == "figurine" ]; then ${BANNER_COMMAND} -f "${BANNER_FONT}" "${BANNER}" | sed "s/^/${BANNER_INDENT}/" else diff --git a/rootfs/etc/profile.d/motd.sh b/rootfs/etc/profile.d/motd.sh index 6a50532a5..525a305cb 100755 --- a/rootfs/etc/profile.d/motd.sh +++ b/rootfs/etc/profile.d/motd.sh @@ -1,9 +1,11 @@ -if [[ $SHLVL -eq 1 ]]; then +if [[ $SHLVL -eq 1 ]] && ! [[ $GEODESIC_MOTD_ENABLED == "false" ]]; then if [ -f "/etc/motd" ]; then - cat "/etc/motd" + source "/etc/motd.sh" fi if [ -n "${MOTD_URL}" ]; then curl --fail --connect-timeout 1 --max-time 1 --silent "${MOTD_URL}" fi fi + +unset GEODESIC_MOTD_ENABLED diff --git a/rootfs/etc/profile.d/oathtool.sh b/rootfs/etc/profile.d/oathtool.sh deleted file mode 100755 index 5fdd7b2d2..000000000 --- a/rootfs/etc/profile.d/oathtool.sh +++ /dev/null @@ -1,11 +0,0 @@ -function mfa() { - local profile="${1:-${AWS_MFA_PROFILE}}" - local file="${AWS_DATA_PATH}/${profile}.mfa" - if [ -f "${file}" ]; then - oathtool --base32 --totp "$(cat ${file})" - elif [ -z "${profile}" ]; then - echo "No MFA profile defined" >&2 - else - echo "No MFA profile for $profile" >&2 - fi -} diff --git a/rootfs/etc/profile.d/ssh-agent.sh b/rootfs/etc/profile.d/ssh-agent.sh deleted file mode 100755 index 7927d1ffe..000000000 --- a/rootfs/etc/profile.d/ssh-agent.sh +++ /dev/null @@ -1,31 +0,0 @@ -export SSH_KEY="${SSH_KEY:-/localhost/.ssh/id_rsa}" - -# Attempt Re-use existing agent if one exists -if [ -f "${SSH_AGENT_CONFIG}" ]; then - echo "* Found SSH agent config" - . "${SSH_AGENT_CONFIG}" -fi - -trap ctrl_c INT - -function ctrl_c() { - echo "* Okay, nevermind =)" - killall -9 ssh-agent - rm -f "${SSH_AUTH_SOCK}" -} - -# Otherwise launch a new agent -if [ -z "${SSH_AUTH_SOCK}" ] || ! [ -e "${SSH_AUTH_SOCK}" ]; then - ssh-agent | grep -v '^echo' >"${SSH_AGENT_CONFIG}" - . "${SSH_AGENT_CONFIG}" - - # Add keys (if any) to the agent - if [ -n "${SSH_KEY}" ] && [ -f "${SSH_KEY}" ]; then - echo "Add your local private SSH key to the key chain. Hit ^C to skip." - ssh-add "${SSH_KEY}" - fi -fi - -# Clean up -trap - INT -unset -f ctrl_c diff --git a/rootfs/etc/profile.d/terraform.sh b/rootfs/etc/profile.d/terraform.sh index 9b2629f97..b47d8c82f 100755 --- a/rootfs/etc/profile.d/terraform.sh +++ b/rootfs/etc/profile.d/terraform.sh @@ -47,5 +47,5 @@ done # Set default plugin cache dir (must not be one of the mirror directories) # https://www.terraform.io/docs/commands/cli-config.html#implied-local-mirror-directories) -export TF_PLUGIN_CACHE_DIR="${TF_PLUGIN_CACHE_DIR:-/localhost/.terraform.d/plugin-cache}" +export TF_PLUGIN_CACHE_DIR="${TF_PLUGIN_CACHE_DIR:-${HOME}/.terraform.d/plugin-cache}" mkdir -p "$TF_PLUGIN_CACHE_DIR" || unset TF_PLUGIN_CACHE_DIR diff --git a/rootfs/templates/bootstrap b/rootfs/templates/bootstrap index 783dd110b..3780e3cef 100755 --- a/rootfs/templates/bootstrap +++ b/rootfs/templates/bootstrap @@ -1,6 +1,6 @@ #!/bin/bash export DOCKER_IMAGE="{{getenv "DOCKER_IMAGE" "cloudposse/geodesic"}}" -export DOCKER_TAG="{{- getenv "DOCKER_TAG" (printf "${1:-%s-%s}" ((index (getenv "GEODESIC_VERSION" | strings.Split " ") 0) | default "dev") (getenv "GEODESIC_OS" "alpine")) -}}" +export DOCKER_TAG="{{- getenv "DOCKER_TAG" (printf "${1:-%s-%s}" ((index (getenv "GEODESIC_VERSION" | strings.Split " ") 0) | default "dev") (getenv "GEODESIC_OS" "debian")) -}}" export APP_NAME=${APP_NAME:-$(basename $DOCKER_IMAGE)} export INSTALL_PATH=${INSTALL_PATH:-/usr/local/bin} export SAFE_INSTALL_PATH="$HOME/.local/bin" # per XDG recommendations diff --git a/rootfs/templates/wrapper b/rootfs/templates/wrapper deleted file mode 100755 index d7bbb91d1..000000000 --- a/rootfs/templates/wrapper +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/env bash -# Geodesic Wrapper Script - -set -o pipefail - -# Geodesic Settings -export GEODESIC_PORT=${GEODESIC_PORT:-$((30000 + $$ % 30000))} - -export GEODESIC_HOST_CWD=$(pwd -P 2>/dev/null || pwd) - -readonly OS=$(uname -s) - -export USER_ID=$(id -u) -export GROUP_ID=$(id -g) - -export options=() -export targets=() - -function require_installed() { - if ! which $1 >/dev/null; then - echo "Cannot find $1 installed on this system. Please install and try again." - exit 1 - fi -} - -function options_to_env() { - local kv - local k - local v - - for option in ${options[@]}; do - kv=(${option/=/ }) - k=${kv[0]} # Take first element as key - k=${k#--} # Strip leading -- - k=${k//-/_} # Convert dashes to underscores - k=$(echo $k | tr '[:lower:]' '[:upper:]') # Convert to uppercase (bash3 compat) - - v=${kv[1]} # Treat second element as value - v=${v:-true} # Set it to true for boolean flags - - export $k="$v" - done -} - -function debug() { - if [ "${VERBOSE}" == "true" ]; then - echo "[DEBUG] $*" - fi -} - -function run_exit_hooks() { - command -v geodesic_on_exit >/dev/null && geodesic_on_exit -} - -function use() { - DOCKER_ARGS=() - if [ -t 1 ]; then - # Running in terminal - DOCKER_ARGS+=(-it --rm --name="${DOCKER_NAME}" --env LS_COLORS --env TERM --env TERM_COLOR --env TERM_PROGRAM) - - if [ -n "$SSH_AUTH_SOCK" ]; then - if [ "${OS}" == 'Linux' ]; then - # Bind-mount SSH agent socket into container (linux only) - DOCKER_ARGS+=(--volume "$SSH_AUTH_SOCK:$SSH_AUTH_SOCK" - --env SSH_AUTH_SOCK - --env SSH_CLIENT - --env SSH_CONNECTION - --env SSH_TTY - --env USER_ID - --env GROUP_ID) - elif [ "${OS}" == 'Darwin' ] && [ "${GEODESIC_MAC_FORWARD_SOCKET}" == 'true' ]; then - # Bind-mount SSH-agent socket (available in docker-for mac Edge 2.2 release) - # Note that the file/socket /run/host-services/ssh-auth.sock does not exist - # on the host OS, it is in the Moby Linux VM in which the Docker daemon `dockerd` runs. - # See https://github.com/docker/for-mac/issues/410#issuecomment-557613306 - # and https://docs.docker.com/docker-for-mac/osxfs/#namespaces - DOCKER_ARGS+=(--volume /run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock - -e SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock) - fi - fi - else - DOCKER_ARGS=() - fi - - if [ "${GEODESIC_HOST_BINDFS_ENABLED}" = "true" ]; then - if [ "${USER_ID}" = 0 ]; then - echo "# WARNING: Host user is root. This is DANGEROUS." - echo " * Geodesic should not be launched by the host root user." - echo " * Use \"rootless\" mode instead. See https://docs.docker.com/engine/security/rootless/" - echo "# Not enabling BindFS host filesystem mapping because host user is root." - else - echo "# Enabling BindFS mapping of file system owner and group ID." - DOCKER_ARGS+=( - --env GEODESIC_HOST_UID="${USER_ID}" - --env GEODESIC_HOST_GID="${GROUP_ID}" - --env GEODESIC_LOCALHOST="${GEODESIC_LOCALHOST:=/localhost.bindfs}" - --env GEODESIC_BINDFS_OPTIONS - ) - fi - fi - - if [ "${WITH_DOCKER}" == "true" ]; then - # Bind-mount docker socket into container - # Should work on Linux and Mac. - # Note that the mounted /var/run/docker.sock is not a file or - # socket in the Mac host OS, it is in the dockerd VM. - # https://docs.docker.com/docker-for-mac/osxfs/#namespaces - echo "# Enabling docker support. Be sure you install a docker CLI binary{{getenv "DOCKER_INSTALL_PROMPT"}}." - DOCKER_ARGS+=(--volume "/var/run/docker.sock:/var/run/docker.sock") - # Some reports say this is needed for Windows WSL - if [[ $(uname -r) =~ Microsoft$ ]]; then - DOCKER_ARGS+=(--user root) - fi - # NOTE: bind mounting the docker CLI binary is no longer recommended and usually does not work. - # Use a docker image with a docker CLI binary installed that is appropriate to the image's OS. - fi - - if [[ ${GEODESIC_CUSTOMIZATION_DISABLED-false} == false ]]; then - if [ -n "${GEODESIC_TRACE}" ]; then - DOCKER_ARGS+=(--env GEODESIC_TRACE) - fi - - if [ -n "${ENV_FILE}" ]; then - DOCKER_ARGS+=(--env-file ${ENV_FILE}) - fi - - # allow users to override value of GEODESIC_DEFAULT_ENV_FILE - local geodesic_default_env_file=${GEODESIC_DEFAULT_ENV_FILE:-~/.geodesic/env} - if [ -f "${geodesic_default_env_file}" ]; then - DOCKER_ARGS+=(--env-file=${geodesic_default_env_file}) - fi - else - echo "# Disabling user customizations: GEODESIC_CUSTOMIZATION_DISABLED is set and not 'false'" - DOCKER_ARGS+=(--env GEODESIC_CUSTOMIZATION_DISABLED) - fi - - if [ -n "${DOCKER_DNS}" ]; then - DOCKER_ARGS+=("--dns=${DOCKER_DNS}") - fi - - if [ -n "${LOCAL_HOME}" ]; then - local_home=${LOCAL_HOME} - elif [[ $(uname -r) =~ Microsoft$ ]]; then - - # Lookup correct mount path for WSL - mount_path=$(dirname $(findmnt -S C: -t drvfs -no target)) - - windows_user_name=$($mount_path/c/Windows/System32/cmd.exe /c 'echo %USERNAME%' | tr -d '\r') - user_local_app_data=$(cmd.exe /c echo %LOCALAPPDATA% | tr -d '\r' | sed -e 's/\\/\//g') - - if [ -d "$mount_path/c/Users/${windows_user_name}/AppData/Local/lxss/" ]; then - local_home=${user_local_app_data}/lxss${HOME} - else - local restore_nullglob=$(shopt -p nullglob) - shopt -s nullglob - for dir in $mount_path/c/Users/${windows_user_name}/AppData/Local/Packages/CanonicalGroupLimited.Ubuntu*; do - folder_name=$(basename ${dir}) - local_home=${user_local_app_data}/Packages/${folder_name}/LocalState/rootfs${HOME} - break - done - $restore_nullglob - fi - - if [ -z "${local_home}" ]; then - echo "ERROR: can't identify user home directory, you may specify path via LOCAL_HOME variable" - exit 1 - else - echo "Detected Windows Subsystem for Linux, mounting $local_home instead of $HOME" - fi - - else - local_home=${HOME} - fi - - if [ "${local_home}" == "/localhost" ]; then - echo "WARNING: not mounting ${local_home} because it conflicts with geodesic" - else - if [ "${GEODESIC_LOCALHOST:-/localhost}" != "/localhost" ]; then - echo "# Mounting ${local_home} into container at ${GEODESIC_LOCALHOST} with workdir ${GEODESIC_HOST_CWD}" - else - echo "# Mounting ${local_home} into container with workdir ${GEODESIC_HOST_CWD}" - fi - DOCKER_ARGS+=( - --volume="${local_home}:${GEODESIC_LOCALHOST:-/localhost}" - --env LOCAL_HOME="${local_home}" - ) - fi - - if [ -n "${XDG_CONFIG_HOME}" ]; then - DOCKER_ARGS+=(--env XDG_CONFIG_HOME) - fi - - DOCKER_ARGS+=( - --privileged - --publish ${GEODESIC_PORT}:${GEODESIC_PORT} - --name "${DOCKER_NAME}" - --rm - --env GEODESIC_PORT=${GEODESIC_PORT} - --env DOCKER_IMAGE="${DOCKER_IMAGE%:*}" - --env DOCKER_NAME="${DOCKER_NAME}" - --env DOCKER_TAG="${DOCKER_TAG}" - --env GEODESIC_HOST_CWD="${GEODESIC_HOST_CWD}" - ) - - trap run_exit_hooks EXIT - # the extra curly braces around .ID are because this file goes through go template substitution locally before being installed as a shell script - container_id=$(docker ps --filter name="^/${DOCKER_NAME}\$" --format '{{`{{ .ID }}`}}') - if [ -n "$container_id" ]; then - echo "# Attaching to existing ${DOCKER_NAME} session ($container_id)" - if [ $# -eq 0 ]; then - set -- "/bin/bash" "-l" "$@" - fi - docker exec -it --env GEODESIC_HOST_CWD="${GEODESIC_HOST_CWD}" "${DOCKER_NAME}" $* - else - echo "# Starting new ${DOCKER_NAME} session from ${DOCKER_IMAGE}" - echo "# Exposing port ${GEODESIC_PORT}" - [ -z "${GEODESIC_DOCKER_EXTRA_ARGS}" ] || echo "# Launching with extra Docker args: ${GEODESIC_DOCKER_EXTRA_ARGS}" - docker run "${DOCKER_ARGS[@]}" ${GEODESIC_DOCKER_EXTRA_ARGS} ${DOCKER_IMAGE} -l $* - fi -} - -function parse_args() { - while [[ $1 ]]; do - case "$1" in - -h | --help) - targets+=("help") - shift - ;; - -v | --verbose) - export VERBOSE=true - shift - ;; - --*) - options+=("${1}") - shift - ;; - --) # End of all options - shift - ;; - -*) - echo "Error: Unknown option: $1" >&2 - exit 1 - ;; - *=*) - declare -g "${1}" - shift - ;; - *) - targets+=("${1}") - shift - ;; - esac - done -} - -function uninstall() { - echo "# Uninstalling ${DOCKER_NAME}..." - docker rm -f ${DOCKER_NAME} >/dev/null 2>&1 || true - docker rmi -f ${DOCKER_IMAGE} >/dev/null 2>&1 || true - echo "# Not deleting $0" - exit 0 -} - -function update() { - echo "# Installing the latest version of ${DOCKER_IMAGE}" - docker run --rm ${DOCKER_IMAGE} | bash -s ${DOCKER_TAG} - if [ $? -eq 0 ]; then - echo "# ${DOCKER_IMAGE} has been updated." - exit 0 - else - echo "Failed to update ${DOCKER_IMAGE}" - exit 1 - fi -} - -function stop() { - echo "# Stopping ${DOCKER_NAME}..." - exec docker kill ${DOCKER_NAME} >/dev/null 2>&1 -} - -function help() { - echo "Usage: $0 [target] ARGS" - echo "" - echo " Targets:" - echo " update Upgrade geodesic wrapper shell" - echo " stop Stop a running shell" - echo " uninstall Remove geodesic image" - echo " Enter into a shell" - echo "" - echo " Arguments:" - echo " --env-file=... Pass an environment file containing key=value pairs" - echo "" -} - -require_installed tr -require_installed grep - -parse_args "$@" -options_to_env - -# Docker settings -export DOCKER_IMAGE="{{getenv "DOCKER_IMAGE" "cloudposse/geodesic"}}" -export DOCKER_TAG="{{getenv "DOCKER_TAG" "${DOCKER_TAG:-dev}"}}" -export DOCKER_NAME="{{getenv "APP_NAME" "${DOCKER_NAME:-$(basename $DOCKER_IMAGE)}"}}" - -if [ -n "${GEODESIC_NAME}" ]; then - export DOCKER_NAME=$(basename "${GEODESIC_NAME:-}") -fi - -if [ -n "${GEODESIC_TAG}" ]; then - export DOCKER_TAG=${GEODESIC_TAG} -fi - -if [ -n "${GEODESIC_IMAGE}" ]; then - export DOCKER_IMAGE=${GEODESIC_IMAGE:-${DOCKER_IMAGE}:${DOCKER_TAG}} -else - export DOCKER_IMAGE=${DOCKER_IMAGE}:${DOCKER_TAG} -fi - -export DOCKER_DNS=${DNS:-${DOCKER_DNS}} - -if [ "${GEODESIC_SHELL}" == "true" ]; then - echo "Cannot run while in a geodesic shell" - exit 1 -fi - -if [ -z "${DOCKER_IMAGE}" ]; then - echo "Error: --image not specified (E.g. --image=cloudposse/foobar.example.com:1.0)" - exit 1 -fi - -require_installed docker - -docker ps >/dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "Unable to communicate with docker daemon. Make sure your environment is properly configured and then try again." - exit 1 -fi - -if [ -z "$targets" ]; then - # Execute default target - targets=("use") -fi - -for target in $targets; do - if [ "$target" == "update" ]; then - update - elif [ "$target" == "uninstall" ]; then - uninstall - elif [ "$target" == "stop" ]; then - stop - elif [ "$target" == "use" ]; then - use - elif [ "$target" == "help" ]; then - help - else - echo "Unknown target: $target" - exit 1 - fi -done diff --git a/rootfs/templates/wrapper-body.sh b/rootfs/templates/wrapper-body.sh new file mode 100755 index 000000000..7bce8deca --- /dev/null +++ b/rootfs/templates/wrapper-body.sh @@ -0,0 +1,498 @@ +# Default directory mounts for the user's home directory +homedir_default_mounts=".aws,.config,.emacs.d,.geodesic,.gitconfig,.kube,.ssh,.terraform.d" + +function require_installed() { + if ! command -v $1 >/dev/null 2>&1; then + echo "Cannot find $1 installed on this system. Please install and try again." + exit 1 + fi +} + +## Verify we have the foundations in place + +if [ "${GEODESIC_SHELL}" == "true" ]; then + echo "Cannot run while in a geodesic shell" + exit 1 +fi + +require_installed tr +require_installed grep +require_installed docker + +docker ps >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Unable to communicate with docker daemon. Make sure your environment is properly configured and then try again." + exit 1 +fi + +## Set up the default configuration + +### Geodesic Settings +export GEODESIC_PORT=${GEODESIC_PORT:-$((30000 + $$ % 30000))} + +export GEODESIC_HOST_CWD=$(pwd -P 2>/dev/null || pwd) + +readonly OS=$(uname -s) + +export USER_ID=$(id -u) +export GROUP_ID=$(id -g) + +export options=() +export targets=() + +### Docker defaults + +export DOCKER_DNS=${DNS:-${DOCKER_DNS}} +DOCKER_DETACH_KEYS="ctrl-@,ctrl-[,ctrl-@" + +## Read in custom configuration here, so it can override defaults + +export GEODESIC_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/geodesic" +if ! [ -d "$GEODESIC_CONFIG_HOME" ] && [ -d "$HOME/.geodesic" ]; then + GEODESIC_CONFIG_HOME="$HOME/.geodesic" +fi + +verbose_buffer=() +launch_options="$GEODESIC_CONFIG_HOME/defaults/launch-options.sh" +if [ -f "$launch_options" ]; then + source "$launch_options" && verbose_buffer+=("Configuration loaded from $launch_options") || printf 'Error loading configuration from %s\n' "$launch_options" >&2 +else + verbose_buffer+=("Not found (OK): $launch_options") +fi + +# Wait until here to parse $DOCKER_IMAGE, so that it can be overridden in $GEODESIC_CONFIG_HOME/launch-options.sh + +if [ -n "${GEODESIC_NAME}" ]; then + export DOCKER_NAME=$(basename "${GEODESIC_NAME:-}") +fi + +if [ -n "${GEODESIC_TAG}" ]; then + export DOCKER_TAG=${GEODESIC_TAG} +fi + +if [ -n "${GEODESIC_IMAGE}" ]; then + export DOCKER_IMAGE=${GEODESIC_IMAGE:-${DOCKER_IMAGE}}:${DOCKER_TAG} +else + export DOCKER_IMAGE=${DOCKER_IMAGE}:${DOCKER_TAG} +fi + +if [ -z "${DOCKER_IMAGE}" ]; then + echo "Error: --image not specified (E.g. --image=cloudposse/foobar.example.com:1.0)" + exit 1 +fi + +docker_stage="${DOCKER_IMAGE##*/}" # remove the registry and org +docker_stage="${docker_stage%%:*}" # remove the tag +docker_org="${DOCKER_IMAGE%/*}" # remove the name and tag +# If the docker image is in the form of "docker.io/library/alpine:latest", then docker_org is "docker.io/library". +# Remove the "docker.io/" prefix if it exists. +docker_org="${docker_org#*/}" + +for dir in "$docker_org" "$docker_stage" "$docker_org/$docker_stage"; do + docker_image_launch_options="$GEODESIC_CONFIG_HOME/${dir}/launch-options.sh" + if [ -f "$docker_image_launch_options" ]; then + source "$docker_image_launch_options" && verbose_buffer+=("Configuration loaded from $docker_image_launch_options") || printf 'Error loading configuration from %s' "$docker_image_launch_options" >&2 + else + verbose_buffer+=("Not found (OK): $docker_image_launch_options") + fi +done + +# GEODESIC_CONFIG_HOME="${GEODESIC_CONFIG_HOME#${HOME}/}" + +function parse_args() { + local arg + while [[ $1 ]]; do + arg="$1" + shift + case "$arg" in + -h | --help) + targets+=("help") + ;; + -v | --verbose) + export VERBOSE=true + ;; + --trace) + export GEODESIC_TRACE=custom + ;; + --trace=*) + export GEODESIC_TRACE="${1#*=}" + ;; + --no-custom*) + export GEODESIC_CUSTOMIZATION_DISABLED=true + ;; + --no-motd*) + export GEODESIC_MOTD_ENABLED=false + ;; + --*) + options+=("${arg}") + ;; + --) # End of all options + break + ;; + -*) + echo "Error: Unknown option: ${arg}" >&2 + exit 1 + ;; + *=*) + declare -g "${arg}" + ;; + *) + targets+=("${arg}") + ;; + esac + done +} + +function options_to_env() { + local kv + local k + local v + + for option in ${options[@]}; do + kv=(${option/=/ }) + k=${kv[0]} # Take first element as key + k=${k#--} # Strip leading -- + k=${k//-/_} # Convert dashes to underscores + k=$(echo $k | tr '[:lower:]' '[:upper:]') # Convert to uppercase (bash3 compat) + + v=${kv[1]} # Treat second element as value + v=${v:-true} # Set it to true for boolean flags + + export $k="$v" + done +} + +parse_args "$@" +options_to_env + +[ "$VERBOSE" = "true" ] && [ -n "$verbose_buffer" ] && printf "%s\n" "${verbose_buffer[@]}" + +function debug() { + if [ "${VERBOSE}" == "true" ]; then + echo "[DEBUG] $*" + fi +} + +function _running_shell_count() { + local count=$(docker exec "${DOCKER_NAME}" pgrep -f "^/bin/(ba)?sh -l" 2>/dev/null | wc -l | tr -d " " || true) + [ -n "${count}" ] || count=0 + echo "${count}" +} + +function _on_shell_exit() { + command -v "${ON_SHELL_EXIT:=geodesic_on_exit}" >/dev/null && "${ON_SHELL_EXIT}" +} + +function _on_container_exit() { + export GEODESIC_CONTAINER_EXITING="${CONTAINER_ID:0:12}" + _on_shell_exit + [ -n "${ON_CONTAINER_EXIT}" ] && command -v "${ON_CONTAINER_EXIT}" >/dev/null && "${ON_CONTAINER_EXIT}" +} + +function run_exit_hooks() { + # This runs as soon as the terminal is detached. It may take moments for the shell to actually exit. + # It can then take at least a second for the init process to quit. + # There can then be a further delay before the container exits. + # So we need to build in some delays to allow for these events to occur. + + if [[ ${ONE_SHELL} == "true" ]]; then + # We can expect the Docker container to exit quickly, and do not need to report on it. + _on_container_exit + return 0 + fi + + # Initial count of running shells + shells=$(_running_shell_count) + if [ "$shells" -gt 1 ]; then + # Even if our shell is included in the count, we know there are extra shells running. + # Wait for our shell to quit and count again + sleep 1 + shells=$(_running_shell_count) + if [ "$shells" -eq 0 ]; then # coincidence, other shells quit too + echo Other shells quit, too, and Docker container exited + _on_container_exit + return 0 + fi + else # 1 or zero shells. The 1 might be ours, so we wait for it to quit. + for i in {1..9}; do + if [ $i -eq 9 ] || [ $(docker ps -q --filter "id=${CONTAINER_ID:0:12}" | wc -l | tr -d " ") -eq 0 ]; then + break + fi + [ $i -lt 8 ] && sleep 1 + done + if [ $i -eq 9 ]; then + shells=$(_running_shell_count) + if [ "$shells" -eq 0 ]; then + printf 'All shells terminated, but docker container still running.\n' >&2 + printf 'Forcibly kill it with:\n\n docker kill %s\n\n' "${DOCKER_NAME}" >&2 + _on_shell_exit + return 6 + fi + else + echo Docker container exited + _on_container_exit + return 0 + fi + fi + + # If we get here, container is still running and shells != 0 + echo Docker container still running + [ "$shells" -eq 1 ] && echo -n "Quit 1 other shell " || echo -n "Quit $shells other shells " + echo 'to terminate, or force quit with `docker kill '"${DOCKER_NAME}"'`' + _on_shell_exit +} + +function use() { + DOCKER_ARGS=() + if [ -t 1 ]; then + # Running in terminal + DOCKER_ARGS+=(-it --rm --env LS_COLORS --env TERM --env TERM_COLOR --env TERM_PROGRAM --env GEODESIC_MOTD_ENABLED) + if [ -n "$SSH_AUTH_SOCK" ]; then + DOCKER_ARGS+=(--volume /run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock + -e SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock) + fi + # Some settings from the host environment need to propagate into the container + # Set them explicitly so they do not have to be exported in `launch-options.sh` + for v in GEODESIC_CONFIG_HOME GEODESIC_MOTD_ENABLED GEODESIC_TERM_COLOR_AUTO; do + if [ -n "${!v+x}" ]; then + DOCKER_ARGS+=(--env "$v=${!v}") + fi + done + fi + + mount_dir="" + if [ -n "${GEODESIC_HOST_BINDFS_ENABLED+x}" ]; then + echo "# WARNING: GEODESIC_HOST_BINDFS_ENABLED is deprecated. Use MAP_FILE_OWNERSHIP instead." + export MAP_FILE_OWNERSHIP="${GEODESIC_HOST_BINDFS_ENABLED}" + fi + if [ "${MAP_FILE_OWNERSHIP}" = "true" ]; then + if [ "${USER_ID}" = 0 ]; then + echo "# WARNING: Host user is root. This is DANGEROUS." + echo " * Geodesic should not be launched by the host root user." + echo " * Use \"rootless\" mode instead. See https://docs.docker.com/engine/security/rootless/" + echo "# Not enabling BindFS host filesystem mapping because host user is root, same as container user." + else + echo "# Enabling explicit mapping of file owner and group ID between container and host." + mount_dir="/.BINDFS" + DOCKER_ARGS+=( + --env GEODESIC_HOST_UID="${USER_ID}" + --env GEODESIC_HOST_GID="${GROUP_ID}" + --env GEODESIC_BINDFS_OPTIONS + --env MAP_FILE_OWNERSHIP=true + ) + fi + fi + + if [ "${WITH_DOCKER}" == "true" ]; then + # Bind-mount docker socket into container + # Should work on Linux and Mac. + # Note that the mounted /var/run/docker.sock is not a file or + # socket in the Mac host OS, it is in the dockerd VM. + # https://docs.docker.com/docker-for-mac/osxfs/#namespaces + echo "# Enabling docker support. Be sure you install a docker CLI binary${docker_install_prompt}." + DOCKER_ARGS+=(--volume "/var/run/docker.sock:/var/run/docker.sock") + # NOTE: bind mounting the docker CLI binary is no longer recommended and usually does not work. + # Use a docker image with a docker CLI binary installed that is appropriate to the image's OS. + fi + + if [[ ${GEODESIC_CUSTOMIZATION_DISABLED-false} == false ]]; then + if [ -n "${GEODESIC_TRACE}" ]; then + DOCKER_ARGS+=(--env GEODESIC_TRACE) + fi + + if [ -n "${ENV_FILE}" ]; then + DOCKER_ARGS+=(--env-file ${ENV_FILE}) + fi + else + echo "# Disabling user customizations: GEODESIC_CUSTOMIZATION_DISABLED is set and not 'false'" + DOCKER_ARGS+=(--env GEODESIC_CUSTOMIZATION_DISABLED) + fi + + if [ -n "${DOCKER_DNS}" ]; then + DOCKER_ARGS+=("--dns=${DOCKER_DNS}") + fi + + # Mount the user's home directory into the container + # but allow them to specify some directory other than their actual home directory + if [ -n "${LOCAL_HOME}" ]; then + local_home=${LOCAL_HOME} + else + local_home=${HOME} + fi + + # Although we call it "dirs", it can be files too + export GEODESIC_HOMEDIR_MOUNTS="" + DOCKER_ARGS+=(--env GEODESIC_HOMEDIR_MOUNTS --env LOCAL_HOME="${local_home}") + [ -z "${HOMEDIR_MOUNTS+x}" ] && HOMEDIR_MOUNTS=("${homedir_default_mounts[@]}") + IFS=, read -ra HOMEDIR_MOUNTS <<<"${HOMEDIR_MOUNTS}" + IFS=, read -ra HOMEDIR_ADDITIONAL_MOUNTS <<<"${HOMEDIR_ADDITIONAL_MOUNTS}" + for dir in "${HOMEDIR_MOUNTS[@]}" "${HOMEDIR_ADDITIONAL_MOUNTS[@]}"; do + if [ -d "${local_home}/${dir}" ] || [ -f "${local_home}/${dir}" ]; then + DOCKER_ARGS+=(--volume="${local_home}/${dir}:${mount_dir}${local_home}/${dir}") + GEODESIC_HOMEDIR_MOUNTS+="${dir}|" + debug "Mounting '${local_home}/${dir}' into container'" + else + debug "Not mounting '${local_home}/${dir}' into container because it is not a directory or file" + fi + done + + # WORKSPACE_MOUNT is the directory in the container that is to be the mount point for the host filesystem + WORKSPACE_MOUNT="${WORKSPACE_MOUNT:-/workspace}" + # WORKSPACE_HOST_DIR is the directory on the host that is to be the working directory + WORKSPACE_FOLDER_HOST_DIR="${WORKSPACE_FOLDER_HOST_DIR:-${GEODESIC_HOST_CWD}}" + git_root=$(git rev-parse --show-toplevel 2>/dev/null) + if [ -z "${git_root}" ] || [ "$git_root" = "${WORKSPACE_FOLDER_HOST_DIR}" ]; then + # WORKSPACE_HOST_PATH is the directory on the host that is to be mounted into the container + WORKSPACE_MOUNT_HOST_DIR="${WORKSPACE_FOLDER_HOST_DIR}" + WORKSPACE_FOLDER="${WORKSPACE_FOLDER:-${WORKSPACE_MOUNT}}" + else + # If we are in a git repo, mount the git root into the container at /workspace + WORKSPACE_MOUNT_HOST_DIR="${git_root}" + WORKSPACE_FOLDER="${WORKSPACE_FOLDER:-${WORKSPACE_MOUNT}/${WORKSPACE_FOLDER_HOST_DIR#${git_root}/}}" + fi + + echo "# Mounting '${WORKSPACE_MOUNT_HOST_DIR}' into container at '${WORKSPACE_MOUNT}'" + echo "# Setting container working directory to '${WORKSPACE_FOLDER}'" + + DOCKER_ARGS+=( + --volume="${WORKSPACE_MOUNT_HOST_DIR}:${mount_dir}${WORKSPACE_MOUNT_HOST_DIR}" + --env WORKSPACE_MOUNT_HOST_DIR="${WORKSPACE_MOUNT_HOST_DIR}" + --env WORKSPACE_MOUNT="${WORKSPACE_MOUNT}" + --env WORKSPACE_FOLDER="${WORKSPACE_FOLDER}" + ## TODO: Remove legacy vars + # --env GEODESIC_LOCALHOST="${WORKSPACE_MOUNT}" + # --env GEODESIC_WORKDIR="${WORKSPACE_FOLDER}" + # --env HOME="/root" + ) + + ###### TODO + ## Need to distinguish from mount point, which could be bindfs, from read point + ## + ## Everything under $HOME is mounted under $GEODESIC_LOCALHOST + ## Everything not under $HOME is mounted under $GEODESIC_LOCALHOST/_HOST + ## + ## ln -s /workspace $HOME + ## for d in $GEODESIC_LOCALHOST/_HOST/* + ## for d in $(shopt -s nullglob; $GEODESIC_LOCALHOST/_HOST/*); do ln $x; done + + # Mount the host mounts wherever the users asks for them to be mounted + export GEODESIC_HOST_MOUNTS="" + IFS=, read -ra HOST_MOUNTS <<<"${HOST_MOUNTS}" + for dir in "${HOST_MOUNTS[@]}"; do + d="${dir%%:*}" + if [ -d "${d}" ]; then + if [ "${dir}" != "${d}" ]; then + DOCKER_ARGS+=(--volume="${d}:${mount_dir}${dir#*:}") + debug "Mounting ${d} into container at ${dir#*:}" + GEODESIC_HOST_MOUNTS+="${dir#*:}|" + else + DOCKER_ARGS+=(--volume="${d}:${mount_dir}${d}") + debug "Mounting ${d} into container at ${d}" + GEODESIC_HOST_MOUNTS+="${d}|" + fi + fi + done + + DOCKER_ARGS+=(--env GEODESIC_HOST_MOUNTS) + + #echo "Computed DOCKER_ARGS:" + #printf " %s\n" "${DOCKER_ARGS[@]}" + + DOCKER_ARGS+=( + --privileged + --publish ${GEODESIC_PORT}:${GEODESIC_PORT} + --rm + --env GEODESIC_PORT=${GEODESIC_PORT} + --env DOCKER_IMAGE="${DOCKER_IMAGE%:*}" + --env DOCKER_NAME="${DOCKER_NAME}" + --env DOCKER_TAG="${DOCKER_TAG}" + --env GEODESIC_HOST_CWD="${GEODESIC_HOST_CWD}" + ) + + trap run_exit_hooks EXIT + if [ "$ONE_SHELL" = "true" ]; then + DOCKER_NAME="${DOCKER_NAME}-$(date +%d%H%M%S)" + echo "# Starting single shell ${DOCKER_NAME} session from ${DOCKER_IMAGE}" + echo "# Exposing port ${GEODESIC_PORT}" + [ -z "${GEODESIC_DOCKER_EXTRA_ARGS}" ] || echo "# Launching with extra Docker args: ${GEODESIC_DOCKER_EXTRA_ARGS}" + docker run --name "${DOCKER_NAME}" "${DOCKER_ARGS[@]}" ${GEODESIC_DOCKER_EXTRA_ARGS} ${DOCKER_IMAGE} -l $* + else + # the extra curly braces around .ID are because this file goes through go template substitution locally before being installed as a shell script + CONTAINER_ID=$(docker ps --filter name="^/${DOCKER_NAME}\$" --format '{{ .ID }}') + if [ -n "$CONTAINER_ID" ]; then + echo "# Starting shell in already running ${DOCKER_NAME} container ($CONTAINER_ID)" + if [ $# -eq 0 ]; then + set -- "/bin/bash" "-l" "$@" + fi + # We set unusual detach keys because (a) the default first char is ctrl-p, which is used for command history, + # and (b) if you detach from the shell, there is no way to reattach to it, so we want to effectively disable detach. + docker exec -it --detach-keys "ctrl-^,ctrl-[,ctrl-@" --env GEODESIC_HOST_CWD="${GEODESIC_HOST_CWD}" "${DOCKER_NAME}" $* + else + echo "# Running new ${DOCKER_NAME} container from ${DOCKER_IMAGE}" + echo "# Exposing port ${GEODESIC_PORT}" + [ -z "${GEODESIC_DOCKER_EXTRA_ARGS}" ] || echo "# Launching with extra Docker args: ${GEODESIC_DOCKER_EXTRA_ARGS}" + # docker run "${DOCKER_ARGS[@]}" ${GEODESIC_DOCKER_EXTRA_ARGS} ${DOCKER_IMAGE} -l $* + CONTAINER_ID=$(docker run --detach --init --name "${DOCKER_NAME}" "${DOCKER_ARGS[@]}" ${GEODESIC_DOCKER_EXTRA_ARGS} ${DOCKER_IMAGE} /usr/local/sbin/shell-monitor) + echo "# Started session ${CONTAINER_ID:0:12}. Starting shell via \`docker exec\`..." + docker exec -it --detach-keys "ctrl-^,ctrl-[,ctrl-@" --env GEODESIC_HOST_CWD="${GEODESIC_HOST_CWD}" "${DOCKER_NAME}" /bin/bash -l $* + fi + fi + true +} + +_polite_stop() { + name="$1" + [ -n "$name" ] || return 1 + if [ $(docker ps -q --filter "name=${name}" | wc -l | tr -d " ") -eq 0 ]; then + echo "# No running containers found for ${name}" + return + fi + + printf "# Signalling ${name} to stop..." + docker kill -s TERM "${name}" >/dev/null + for i in {1..9}; do + if [ $i -eq 9 ] || [ $(docker ps -q --filter "name=${name}" | wc -l | tr -d " ") -eq 0 ]; then + printf " ${name} stopped gracefully.\n\n" + return 0 + fi + [ $i -lt 8 ] && sleep 1 + done + + printf " ${name} did not stop gracefully. Killing it.\n\n" + docker kill -s TERM "${name}" >/dev/null + return 138 +} + +function stop() { + exec 1>&2 + name=${targets[1]} + if [ -n "$name" ]; then + _polite_stop ${name} + return $? + fi + RUNNING_NAMES=($(docker ps --filter name="^/${DOCKER_NAME}(-\d{8})?\$" --format '{{ .Names }}')) + if [ -z "$RUNNING_NAMES" ]; then + echo "# No running containers found for ${DOCKER_NAME}" + return + fi + if [ ${#RUNNING_NAMES[@]} -eq 1 ]; then + echo "# Stopping ${RUNNING_NAMES[@]}..." + _polite_stop "${RUNNING_NAMES[@]}" + return $? + fi + if [ ${#RUNNING_NAMES[@]} -gt 1 ]; then + echo "# Multiple containers found for ${DOCKER_NAME}:" + for id in "${RUNNING_NAMES[@]}"; do + echo "# ${id}" + done + echo "# Please specify a unique container name." + echo "# $0 stop " + return 1 + fi +} + +if [ "${targets[0]}" == "stop" ]; then + stop +else + use +fi diff --git a/rootfs/templates/wrapper-header.sh.tmpl b/rootfs/templates/wrapper-header.sh.tmpl new file mode 100644 index 000000000..58418944b --- /dev/null +++ b/rootfs/templates/wrapper-header.sh.tmpl @@ -0,0 +1,24 @@ +{{/* + * This is the templated part of the wrapper script. + * It includes the entire header so that we only need 2 files, not 3. + * The bulk of the wrapper script is in the `wrapper-body.sh` file. + */ -}} +#!/usr/bin/env bash + +# Geodesic Wrapper Script +# We keep this compatible with bash 3.2 because that is what macOS ships with. +# Among other things, this means we cannot use [ -v var ] to check if a variable is set, +# so we use [ -n "${var+x}" ] instead. + +set -o pipefail + +# Customized launch settings for this installation + +export DOCKER_IMAGE="{{ getenv "DOCKER_IMAGE" "cloudposse/geodesic" }}" +export DOCKER_TAG="{{ getenv "DOCKER_TAG" "${DOCKER_TAG:-dev}" }}" +export DOCKER_NAME="{{ getenv "APP_NAME" "${DOCKER_NAME:-$(basename $DOCKER_IMAGE)}" }}" + +# Per OS settings +docker_install_prompt="{{ getenv "DOCKER_INSTALL_PROMPT" }}" + +## End of installation configuration diff --git a/rootfs/usr/local/bin/boot b/rootfs/usr/local/bin/boot index 7f238b7ff..d625008d1 100755 --- a/rootfs/usr/local/bin/boot +++ b/rootfs/usr/local/bin/boot @@ -11,25 +11,26 @@ elif [[ $1 = "wrapper" ]]; then color color "########################################################################################" color "# This is the end of the script that installs Geodesic. You should not be seeing this." - color "# This should have been piped into bash. Use the following to install the script that runs" + color "# This should have been piped into bash. Use the following to install the script that" + color "# runs Geodesic with all its features (the recommended way to use Geodesic):" elif [[ -n $DOCKER_IMAGE ]] && [[ -n $DOCKER_TAG ]]; then exec /usr/local/bin/init else function color() { echo "$*" >&2; } color "########################################################################################" color "# Attach a terminal (docker run --rm --it ...) if you want to run a shell." - color "# Run the following to install the script that runs " + color "# Run the following to install the script that runs Geodesic " + color "# with all its features (the recommended way to use Geodesic):" fi source /etc/os-release || true # ignore exit code -color "# Geodesic with all its features (the recommended way to use Geodesic):" color "#" -color "# docker run --rm ${DOCKER_IMAGE:-cloudposse/geodesic}:${DOCKER_TAG:-latest${ID:+-$ID}} init | bash" +color "# docker run --rm ${DOCKER_IMAGE:-cloudposse/geodesic}:${DOCKER_TAG:-dev${ID:+-$ID}} init | bash" color "#" color "# After that, you should be able to launch Geodesic just by typing" color "#" -color "# geodesic" +color "# $(basename ${DOCKER_IMAGE:-geodesic})" color "#" color "########################################################################################" echo diff --git a/rootfs/usr/local/bin/wrapper b/rootfs/usr/local/bin/wrapper index 2a28f2312..8ce4b72f1 100755 --- a/rootfs/usr/local/bin/wrapper +++ b/rootfs/usr/local/bin/wrapper @@ -1,3 +1,4 @@ #!/bin/bash export DOCKER_INSTALL_PROMPT=" ($(install-docker-command))" || unset DOCKER_INSTALL_PROMPT -gomplate -f /templates/wrapper +gomplate -f /templates/wrapper-header.sh.tmpl +cat /templates/wrapper-body.sh diff --git a/rootfs/usr/local/sbin/shell-monitor b/rootfs/usr/local/sbin/shell-monitor new file mode 100755 index 000000000..f248d2703 --- /dev/null +++ b/rootfs/usr/local/sbin/shell-monitor @@ -0,0 +1,44 @@ +#!/bin/bash + +# Function to count active shell sessions +count_shells() { + pgrep -f "^/bin/(ba)?sh -l" | wc -l +} + +kill_shells() { + pkill -HUP -f "^/bin/(ba)?sh -l" + for i in {1..4}; do + [ $(count_shells) -eq 0 ] && return 0 + sleep 1 + done + + pkill -TERM -f "^/bin/(ba)?sh -l" + + for i in {1..3}; do + [ $(count_shells) -eq 0 ] && return 0 + sleep 1 + done + pkill -KILL -f "^/bin/(ba)?sh -l" + return 137 +} + +trap 'kill_shells; exit $?' TERM HUP INT QUIT EXIT + +# Wait up to 4 seconds for the first connection +i=0 +while [ $(count_shells) -eq 0 ]; do + sleep 1 + i=$((i + 1)) + if [ $i -ge 4 ]; then + echo "No shell sessions detected after 4 seconds, exiting..." + exit 1 + fi +done + +# Monitor shell sessions and exit when none remain +while [ $(count_shells) -gt 0 ]; do + sleep 1 +done + +# Clean up and exit +exit 0