Skip to content

Commit

Permalink
Support experimental Docker builds
Browse files Browse the repository at this point in the history
* Simplify .dockerignore
* Update dependency with CVE
* Fixed EOFError on running with no arguments
* Added tests
* Updated documentation
* Added build / push process to .github/
  • Loading branch information
pcmxgti committed Feb 24, 2023
1 parent b26d2fd commit 104c828
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 59 deletions.
43 changes: 8 additions & 35 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,35 +1,8 @@
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.git-blame-ignore-revs
**/.project
**/.settings
**/.toolstarget
**/.tox
**/.vs
**/.vscode
**/.github
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/env
**/venv
**/docs
**/tests
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
AUTHORS.md
LICENSE.txt
README.md
**
.**
!.dockerignore
!requirements.txt
!AUTHORS.md
!LICENSE.txt
!README.md
!tokendito/*.py
70 changes: 70 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Publish to Dockerhub
on:
push:
branches:
- main
tags:
- '[0-9]+.[0-9]+.[0-9]+'

env:
REGISTRY: docker.io
IMAGE_NAME: tokendito/tokendito

jobs:
dockerhubpublish:
name: Build and Publish Docker Container
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- { platform: "linux/arm64", platform-tag: "arm64" }
- { platform: "linux/amd64", platform-tag: "amd64" }
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: tokendito
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build container
uses: docker/build-push-action@v4
with:
context: .
push: false
load: true
platforms: ${{ matrix.platform }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
env:
DOCKER_CONTENT_TRUST: 1
- name: Sign and push container image
uses: sudo-bot/action-docker-sign@latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
with:
image-ref: "${{ steps.meta.outputs.tags }}"
private-key-id: "${{ secrets.DOCKER_PRIVATE_KEY_ID }}"
private-key: "${{ secrets.DOCKER_PRIVATE_KEY }}"
private-key-passphrase: "${{ secrets.DOCKER_PRIVATE_KEY_PASSPHRASE }}"
File renamed without changes.
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Install pip requirements
COPY requirements.txt .
RUN python -m pip install -r requirements.txt
COPY requirements.txt /app/requirements.txt
RUN python -m pip install -r /app/requirements.txt

WORKDIR /app
COPY . /app
Expand All @@ -17,4 +17,4 @@ COPY . /app
RUN adduser -u 5678 --disabled-password --gecos "" tokendito && chown -R tokendito /app
USER tokendito

ENTRYPOINT ["python", "tokendito/tokendito.py"]
ENTRYPOINT ["python", "tokendito/tokendito.py"]
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Consult [additional notes](https://github.com/dowjones/tokendito/blob/main/docs/

## Requirements

- Python 3.7+
- Python 3.7+, or a working Docker environment
- AWS account(s) federated with Okta

Tokendito is compatible with Python 3 and can be installed with either
Expand All @@ -61,20 +61,20 @@ guide](https://github.com/dowjones/tokendito/blob/main/docs/README.md#multi-tile

## Docker

Using Docker eliminates the need to install tokendito and its requirements.
Using Docker eliminates the need to install tokendito and its requirements. We are providing experimental Docker image support in [Dockerhub](https://hub.docker.com/r/tokendito/tokendito)

### Building the container image
### Running the container image

Run tokendito with the `docker run` command. Tokendito supports [DCT](https://docs.docker.com/engine/security/trust/), and we encourage you to enforce image signature validation before running any containers.

``` txt
docker image build --pull --tag "tokendito:latest" .
export DOCKER_CONTENT_TRUST=1
```

### Running the container image

Run tokendito with the `docker run` command
then

``` txt
docker run tokendito --version
docker run --rm -it tokendito --version
```

You must map a volume in the Docker command to allow tokendito to write AWS credentials to your local system for use. This is done with the `-v` flag. See [Docker documentation](https://docs.docker.com/engine/reference/commandline/run/#-mount-volume--v---read-only) for help setting the syntax. The following directories are used by tokendito and should be considered when mapping volumes:
Expand All @@ -84,23 +84,23 @@ You must map a volume in the Docker command to allow tokendito to write AWS cred

These can be covered by mapping a single volume to both the host and container users' home directories (`/home/tokendito/` is the home directory in the container and must be explicitly defined). You may also map multiple volumes if you have custom configuration locations and require granularity.

Be sure to set the `-ti` flags to enable an interactive terminal session.
Be sure to set the `-it` flags to enable an interactive terminal session.

``` txt
docker run -ti -v ${home}:/home/tokendito/ tokendito
docker run --rm -it -v ${home}:/home/tokendito/ tokendito
```

Tokendito command line arguments are supported as well.

``` txt
docker run -ti -v ${home}:/home/tokendito/ tokendito `
--okta-tile https://acme.okta.com/home/amazon_aws/000000000000000000x0/123 `
--username username@example.com `
--okta-mfa push `
--aws-output json `
--aws-region us-east-1 `
--aws-profile my-profile-name `
--aws-role-arn arn:aws:iam::000000000000:role/role-name
docker run --rm -it -v ${home}:/home/tokendito/ tokendito \
--okta-tile https://acme.okta.com/home/amazon_aws/000000000000000000x0/123 \
--username username@example.com \
--okta-mfa push \
--aws-output json \
--aws-region us-east-1 \
--aws-profile my-profile-name \
--aws-role-arn arn:aws:iam::000000000000:role/role-name \
```

Tokendito profiles are supported while using containers provided the proper volume mapping exists.
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Or you can put your parameters into a single [profile](tokendito.ini.md) and ref
okta_aws_tile = https://acme.oktapreview.com/home/amazon_aws/b07384d113edec49eaa6/123
okta_username = jane.doe@acme.com
okta_mfa = push
role_arn = arn:aws:iam::123456789000:role/engineer
aws_role_arn = arn:aws:iam::123456789000:role/engineer
```

And execute:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
beautifulsoup4>=4.6.0
botocore>=1.12.36
certifi>=2022.12.07
platformdirs>=2.5.4
requests>=2.19.0
35 changes: 35 additions & 0 deletions tests/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,35 @@ def test_import_location():
assert imported_path.startswith(local_path)


def test_tty_assertion():
"""Test the availability of stdin."""
import os
import sys
from tokendito.user import tty_assertion

# Save for reuse
old_stdin = sys.stdin
# Test for NoneType
with pytest.raises(SystemExit) as err:
sys.stdin = None
tty_assertion()
assert err.value.code == 1

# Test for null descriptor
with pytest.raises(SystemExit) as err:
sys.stdin = open(os.devnull, "w")
tty_assertion()
assert err.value.code == 1

sys.stdin = old_stdin
# Test for closed descriptor
with pytest.raises(SystemExit) as err:
sys.stdin = old_stdin
os.close(sys.stdin.fileno())
tty_assertion()
assert err.value.code == 1


def test_semver_version():
"""Ensure the package version is semver compliant."""
from tokendito import __version__ as version
Expand Down Expand Up @@ -1055,6 +1084,7 @@ def test_get_mfa_response():
def test_config_object():
"""Test proper initialization of the Config object."""
import json
import sys
from tokendito import Config

# Test for invalid assignments to the object
Expand Down Expand Up @@ -1098,6 +1128,11 @@ def test_config_object():
# Check that default values from the original object are kept
assert pytest_config.get_defaults()["aws"]["region"] == pytest_config.aws["region"]

# Check that we set encoding correctly when there is no stdin
sys.stdin = None
pytest_config = Config()
assert pytest_config.user["encoding"] == "utf-8"


def test_loglevel_collected_from_env(monkeypatch):
"""Ensure that the loglevel collected from env vars."""
Expand Down
6 changes: 5 additions & 1 deletion tokendito/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
class Config(object):
"""Creates configuration variables for the application."""

_default_encoding = "utf-8"
if getattr(sys, "stdin") is not None:
_default_encoding = sys.stdin.encoding

# Instantiated objects can get Class defaults with get_defaults()
_defaults = dict(
user=dict(
Expand All @@ -29,7 +33,7 @@ class Config(object):
user_config_dir(appname=__title__, appauthor=False), f"{__title__}.ini"
),
config_profile="default",
encoding=sys.stdin.encoding,
encoding=_default_encoding,
loglevel="INFO",
log_output_file="",
mask_items=[],
Expand Down
16 changes: 15 additions & 1 deletion tokendito/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,6 @@ def get_interactive_config(tile=None, org=None, username=""):

# We need either one of these two:
while not validate_okta_org(org) and not validate_okta_tile(tile):
print("\n\nPlease enter either your Organization URL, a tile URL, or both.")
org = get_org()
tile = get_tile()

Expand Down Expand Up @@ -857,6 +856,7 @@ def get_password():
res = ""
logger.debug("Set password.")

tty_assertion()
while res == "":
password = getpass.getpass()
res = password
Expand Down Expand Up @@ -1041,12 +1041,26 @@ def validate_input(value, valid_range):
return integer_validation


def tty_assertion():
"""Ensure that a TTY is present."""
try:
assert os.isatty(sys.stdin.fileno()) is True
except (AttributeError, AssertionError, EOFError, OSError, RuntimeError):
logger.error(
"sys.stdin is not available, and interactive invocation requires stdin to be present. "
"Please check the --help argument and documentation for more details.",
)
sys.exit(1)


def get_input(prompt="-> "):
"""Collect user input for TOTP.
:param prompt: optional string with prompt.
:return user_input: raw from user.
"""
tty_assertion()

user_input = input(f"{prompt}")
logger.debug(f"User input: {user_input}")

Expand Down

0 comments on commit 104c828

Please sign in to comment.