Showing preview only (815K chars total). Download the full file or copy to clipboard to get everything.
Repository: basecamp/kamal
Branch: main
Commit: 453d8d7dc2b4
Files: 313
Total size: 739.5 KB
Directory structure:
gitextract_hqeups_8/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── docker-publish.yml
├── .gitignore
├── .rubocop.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── MIT-LICENSE
├── README.md
├── bin/
│ ├── docs
│ ├── kamal
│ ├── release
│ └── test
├── gemfiles/
│ └── rails_edge.gemfile
├── kamal.gemspec
├── lib/
│ ├── kamal/
│ │ ├── cli/
│ │ │ ├── accessory.rb
│ │ │ ├── alias/
│ │ │ │ └── command.rb
│ │ │ ├── app/
│ │ │ │ ├── assets.rb
│ │ │ │ ├── boot.rb
│ │ │ │ ├── error_pages.rb
│ │ │ │ └── ssl_certificates.rb
│ │ │ ├── app.rb
│ │ │ ├── base.rb
│ │ │ ├── build/
│ │ │ │ ├── clone.rb
│ │ │ │ └── port_forwarding.rb
│ │ │ ├── build.rb
│ │ │ ├── healthcheck/
│ │ │ │ ├── barrier.rb
│ │ │ │ ├── error.rb
│ │ │ │ └── poller.rb
│ │ │ ├── lock.rb
│ │ │ ├── main.rb
│ │ │ ├── proxy.rb
│ │ │ ├── prune.rb
│ │ │ ├── registry.rb
│ │ │ ├── secrets.rb
│ │ │ ├── server.rb
│ │ │ └── templates/
│ │ │ ├── deploy.yml
│ │ │ ├── sample_hooks/
│ │ │ │ ├── docker-setup.sample
│ │ │ │ ├── post-app-boot.sample
│ │ │ │ ├── post-deploy.sample
│ │ │ │ ├── post-proxy-reboot.sample
│ │ │ │ ├── pre-app-boot.sample
│ │ │ │ ├── pre-build.sample
│ │ │ │ ├── pre-connect.sample
│ │ │ │ ├── pre-deploy.sample
│ │ │ │ └── pre-proxy-reboot.sample
│ │ │ └── secrets
│ │ ├── cli.rb
│ │ ├── commander/
│ │ │ └── specifics.rb
│ │ ├── commander.rb
│ │ ├── commands/
│ │ │ ├── accessory/
│ │ │ │ └── proxy.rb
│ │ │ ├── accessory.rb
│ │ │ ├── app/
│ │ │ │ ├── assets.rb
│ │ │ │ ├── containers.rb
│ │ │ │ ├── error_pages.rb
│ │ │ │ ├── execution.rb
│ │ │ │ ├── images.rb
│ │ │ │ ├── logging.rb
│ │ │ │ └── proxy.rb
│ │ │ ├── app.rb
│ │ │ ├── auditor.rb
│ │ │ ├── base.rb
│ │ │ ├── builder/
│ │ │ │ ├── base.rb
│ │ │ │ ├── clone.rb
│ │ │ │ ├── cloud.rb
│ │ │ │ ├── hybrid.rb
│ │ │ │ ├── local.rb
│ │ │ │ ├── pack.rb
│ │ │ │ └── remote.rb
│ │ │ ├── builder.rb
│ │ │ ├── docker.rb
│ │ │ ├── hook.rb
│ │ │ ├── lock.rb
│ │ │ ├── proxy.rb
│ │ │ ├── prune.rb
│ │ │ ├── registry.rb
│ │ │ └── server.rb
│ │ ├── commands.rb
│ │ ├── configuration/
│ │ │ ├── accessory.rb
│ │ │ ├── alias.rb
│ │ │ ├── boot.rb
│ │ │ ├── builder.rb
│ │ │ ├── docs/
│ │ │ │ ├── accessory.yml
│ │ │ │ ├── alias.yml
│ │ │ │ ├── boot.yml
│ │ │ │ ├── builder.yml
│ │ │ │ ├── configuration.yml
│ │ │ │ ├── env.yml
│ │ │ │ ├── logging.yml
│ │ │ │ ├── proxy.yml
│ │ │ │ ├── registry.yml
│ │ │ │ ├── role.yml
│ │ │ │ ├── servers.yml
│ │ │ │ ├── ssh.yml
│ │ │ │ └── sshkit.yml
│ │ │ ├── env/
│ │ │ │ └── tag.rb
│ │ │ ├── env.rb
│ │ │ ├── logging.rb
│ │ │ ├── proxy/
│ │ │ │ ├── boot.rb
│ │ │ │ └── run.rb
│ │ │ ├── proxy.rb
│ │ │ ├── registry.rb
│ │ │ ├── role.rb
│ │ │ ├── servers.rb
│ │ │ ├── ssh.rb
│ │ │ ├── sshkit.rb
│ │ │ ├── validation.rb
│ │ │ ├── validator/
│ │ │ │ ├── accessory.rb
│ │ │ │ ├── alias.rb
│ │ │ │ ├── builder.rb
│ │ │ │ ├── configuration.rb
│ │ │ │ ├── env.rb
│ │ │ │ ├── proxy.rb
│ │ │ │ ├── registry.rb
│ │ │ │ ├── role.rb
│ │ │ │ └── servers.rb
│ │ │ ├── validator.rb
│ │ │ └── volume.rb
│ │ ├── configuration.rb
│ │ ├── docker.rb
│ │ ├── env_file.rb
│ │ ├── git.rb
│ │ ├── secrets/
│ │ │ ├── adapters/
│ │ │ │ ├── aws_secrets_manager.rb
│ │ │ │ ├── base.rb
│ │ │ │ ├── bitwarden.rb
│ │ │ │ ├── bitwarden_secrets_manager.rb
│ │ │ │ ├── doppler.rb
│ │ │ │ ├── enpass.rb
│ │ │ │ ├── gcp_secret_manager.rb
│ │ │ │ ├── last_pass.rb
│ │ │ │ ├── one_password.rb
│ │ │ │ ├── passbolt.rb
│ │ │ │ └── test.rb
│ │ │ ├── adapters.rb
│ │ │ └── dotenv/
│ │ │ └── inline_command_substitution.rb
│ │ ├── secrets.rb
│ │ ├── sshkit_with_ext.rb
│ │ ├── tags.rb
│ │ ├── utils/
│ │ │ └── sensitive.rb
│ │ ├── utils.rb
│ │ └── version.rb
│ └── kamal.rb
└── test/
├── cli/
│ ├── accessory_test.rb
│ ├── app_test.rb
│ ├── build_test.rb
│ ├── cli_test_case.rb
│ ├── lock_test.rb
│ ├── main_test.rb
│ ├── proxy_test.rb
│ ├── prune_test.rb
│ ├── registry_test.rb
│ ├── secrets_test.rb
│ └── server_test.rb
├── commander_test.rb
├── commands/
│ ├── accessory_test.rb
│ ├── app_test.rb
│ ├── auditor_test.rb
│ ├── builder_test.rb
│ ├── docker_test.rb
│ ├── hook_test.rb
│ ├── lock_test.rb
│ ├── proxy_test.rb
│ ├── prune_test.rb
│ ├── registry_test.rb
│ └── server_test.rb
├── configuration/
│ ├── accessory_test.rb
│ ├── boot_test.rb
│ ├── builder_test.rb
│ ├── env/
│ │ └── tags_test.rb
│ ├── env_test.rb
│ ├── proxy/
│ │ └── boot_test.rb
│ ├── proxy_test.rb
│ ├── role_test.rb
│ ├── ssh_test.rb
│ ├── sshkit_test.rb
│ ├── validation_test.rb
│ └── volume_test.rb
├── configuration_test.rb
├── env_file_test.rb
├── fixtures/
│ ├── deploy.elsewhere.yml
│ ├── deploy.erb.yml
│ ├── deploy.yml
│ ├── deploy2.yml
│ ├── deploy_for_dest.mars.yml
│ ├── deploy_for_dest.world.yml
│ ├── deploy_for_dest.yml
│ ├── deploy_for_required_dest.world.yml
│ ├── deploy_for_required_dest.yml
│ ├── deploy_primary_web_role_override.yml
│ ├── deploy_simple.yml
│ ├── deploy_with_accessories.yml
│ ├── deploy_with_accessories_on_independent_server.yml
│ ├── deploy_with_accessories_with_different_registries.yml
│ ├── deploy_with_aliases.yml
│ ├── deploy_with_assets.yml
│ ├── deploy_with_boot_strategy.yml
│ ├── deploy_with_cloud_builder.yml
│ ├── deploy_with_env_tags.yml
│ ├── deploy_with_error_pages.yml
│ ├── deploy_with_extensions.yml
│ ├── deploy_with_hybrid_builder.yml
│ ├── deploy_with_local_registry.yml
│ ├── deploy_with_local_registry_and_accessories.yml
│ ├── deploy_with_local_registry_and_remote_builder.yml
│ ├── deploy_with_local_registry_and_remote_builder_with_port.yml
│ ├── deploy_with_multiple_proxy_roles.yml
│ ├── deploy_with_only_workers.yml
│ ├── deploy_with_parallel_roles.yml
│ ├── deploy_with_proxy.yml
│ ├── deploy_with_proxy_roles.yml
│ ├── deploy_with_proxy_run_config.yml
│ ├── deploy_with_proxy_run_config_conflicts.yml
│ ├── deploy_with_remote_builder.yml
│ ├── deploy_with_remote_builder_and_custom_ports.yml
│ ├── deploy_with_roles.yml
│ ├── deploy_with_roles_workers_primary.yml
│ ├── deploy_with_secrets.yml
│ ├── deploy_with_single_accessory.yml
│ ├── deploy_with_two_roles_one_host.yml
│ ├── deploy_with_uncommon_hostnames.yml
│ ├── deploy_without_clone.yml
│ ├── deploy_without_parallel_roles.yml
│ └── files/
│ ├── my.cnf
│ └── structure.sql.erb
├── git_test.rb
├── integration/
│ ├── accessory_test.rb
│ ├── app_test.rb
│ ├── broken_deploy_test.rb
│ ├── docker/
│ │ ├── deployer/
│ │ │ ├── .dockerignore
│ │ │ ├── Dockerfile
│ │ │ ├── app/
│ │ │ │ ├── .kamal/
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── docker-setup
│ │ │ │ │ │ ├── post-app-boot
│ │ │ │ │ │ ├── post-deploy
│ │ │ │ │ │ ├── post-proxy-reboot
│ │ │ │ │ │ ├── pre-app-boot
│ │ │ │ │ │ ├── pre-build
│ │ │ │ │ │ ├── pre-connect
│ │ │ │ │ │ ├── pre-deploy
│ │ │ │ │ │ └── pre-proxy-reboot
│ │ │ │ │ ├── secrets
│ │ │ │ │ └── secrets-common
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ ├── busybox.conf
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_custom_certificate/
│ │ │ │ ├── .kamal/
│ │ │ │ │ └── secrets
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── certs/
│ │ │ │ │ ├── cert.pem
│ │ │ │ │ └── key.pem
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_destinations/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ ├── deploy.production.yml
│ │ │ │ │ ├── deploy.staging.yml
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_parallel_roles/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ ├── default.conf
│ │ │ │ └── error_pages/
│ │ │ │ └── 503.html
│ │ │ ├── app_with_proxied_accessory/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_roles/
│ │ │ │ ├── .kamal/
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── docker-setup
│ │ │ │ │ │ ├── post-deploy
│ │ │ │ │ │ ├── post-proxy-reboot
│ │ │ │ │ │ ├── pre-build
│ │ │ │ │ │ ├── pre-connect
│ │ │ │ │ │ ├── pre-deploy
│ │ │ │ │ │ └── pre-proxy-reboot
│ │ │ │ │ └── secrets
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ ├── default.conf
│ │ │ │ └── error_pages/
│ │ │ │ └── 503.html
│ │ │ ├── app_with_traefik/
│ │ │ │ ├── .kamal/
│ │ │ │ │ └── secrets
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── boot.sh
│ │ │ ├── break_app.sh
│ │ │ ├── setup.sh
│ │ │ └── update_app_rev.sh
│ │ ├── load_balancer/
│ │ │ ├── Dockerfile
│ │ │ └── default.conf
│ │ ├── registry/
│ │ │ ├── Dockerfile
│ │ │ └── boot.sh
│ │ ├── shared/
│ │ │ ├── .dockerignore
│ │ │ ├── Dockerfile
│ │ │ ├── boot.sh
│ │ │ └── registry-dns.conf
│ │ └── vm/
│ │ ├── Dockerfile
│ │ └── boot.sh
│ ├── docker-compose.yml
│ ├── integration_test.rb
│ ├── lock_test.rb
│ ├── main_test.rb
│ └── proxy_test.rb
├── secrets/
│ ├── aws_secrets_manager_adapter_test.rb
│ ├── bitwarden_adapter_test.rb
│ ├── bitwarden_secrets_manager_adapter_test.rb
│ ├── doppler_adapter_test.rb
│ ├── dotenv_inline_command_substitution_test.rb
│ ├── enpass_adapter_test.rb
│ ├── gcp_secret_manager_adapter_test.rb
│ ├── last_pass_adapter_test.rb
│ ├── one_password_adapter_test.rb
│ └── passbolt_adapter_test.rb
├── secrets_test.rb
├── sshkit_dns_retry_test.rb
├── test_helper.rb
└── utils_test.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
rubocop:
name: RuboCop
runs-on: ubuntu-latest
env:
BUNDLE_ONLY: rubocop
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Ruby and install gems
uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Run Rubocop
run: bundle exec rubocop --parallel
tests:
strategy:
fail-fast: false
matrix:
ruby-version:
- "3.2"
- "3.3"
- "3.4"
- "4.0"
gemfile:
- Gemfile
- gemfiles/rails_edge.gemfile
exclude:
- ruby-version: "3.2"
gemfile: gemfiles/rails_edge.gemfile
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Remove gemfile.lock
run: rm Gemfile.lock
- name: Install Ruby
uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true
- name: Configure Docker with VFS storage driver
run: |
sudo systemctl stop docker
sudo systemctl stop docker.socket
sudo mkdir -p /etc/docker /mnt/docker
cat <<'EOF' | sudo tee /etc/docker/daemon.json
{
"storage-driver": "vfs",
"data-root": "/mnt/docker"
}
EOF
sudo rm -rf /var/lib/docker/* /mnt/docker/*
sudo systemctl start docker
timeout 30 sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'
df -h
- name: Run tests
run: bin/test
env:
RUBYOPT: ${{ startsWith(matrix.ruby-version, '3.4.') && '--enable=frozen-string-literal' || '' }}
- name: Check disk usage
if: always()
run: |
df -h
sudo du -sh /mnt/docker
================================================
FILE: .github/workflows/docker-publish.yml
================================================
name: Docker
on:
workflow_dispatch:
inputs:
tagInput:
description: 'Tag'
required: true
release:
types: [created]
tags:
- 'v*'
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
-
name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
-
name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
-
name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Determine version tag
id: version-tag
run: |
INPUT_VALUE="${{ github.event.inputs.tagInput }}"
if [ -z "$INPUT_VALUE" ]; then
INPUT_VALUE="${{ github.ref_name }}"
fi
echo "::set-output name=value::$INPUT_VALUE"
-
name: Build and push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/basecamp/kamal:latest
ghcr.io/basecamp/kamal:${{ steps.version-tag.outputs.value }}
================================================
FILE: .gitignore
================================================
.byebug_history
*.gem
coverage/*
.DS_Store
gemfiles/*.lock
================================================
FILE: .rubocop.yml
================================================
inherit_gem:
rubocop-rails-omakase: rubocop.yml
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
As contributors and maintainers of the Kamal project, we pledge to create a welcoming and inclusive environment for everyone. We value the participation of each member of our community and want all contributors to feel respected and valued.
We are committed to providing a harassment-free experience for everyone, regardless of gender, gender identity and expression, sexual orientation, disability, physical appearance, body size, race, age, or religion (or lack thereof). We do not tolerate harassment of participants in any form.
This code of conduct applies to all Kamal project spaces, including but not limited to project code, issue trackers, chat rooms, and mailing lists. Violations of this code of conduct may result in removal from the project community.
## Our standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Reporting
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a project maintainer. All reports will be kept confidential and will be reviewed and investigated promptly.
We will investigate every complaint and take appropriate action. We reserve the right to remove any content that violates this Code of Conduct, or to temporarily or permanently ban any contributor for other behaviors that we deem inappropriate, threatening, offensive, or harmful.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Kamal development
Thank you for considering contributing to Kamal! This document outlines some guidelines for contributing to this open source project.
Please make sure to review our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing to Kamal.
There are several ways you can contribute to the betterment of the project:
- **Report an issue?** - If the issue isn’t reported, we can’t fix it. Please report any bugs, feature, and/or improvement requests on the [Kamal GitHub Issues tracker](https://github.com/basecamp/kamal/issues).
- **Submit patches** - Do you have a new feature or a fix you'd like to share? [Submit a pull request](https://github.com/basecamp/kamal/pulls)!
- **Write blog articles** - Are you using Kamal? We'd love to hear how you're using it with your projects. Write a tutorial and post it on your blog!
## Issues
If you encounter any issues with the project, please check the [existing issues](https://github.com/basecamp/kamal/issues) first to see if the issue has already been reported. If the issue hasn't been reported, please open a new issue with a clear description of the problem and steps to reproduce it.
## Pull Requests
Please keep the following guidelines in mind when opening a pull request:
- Ensure that your code passes the project's minitests by running ./bin/test.
- Provide a clear and detailed description of your changes.
- Keep your changes focused on a single concern.
- Write clean and readable code that follows the project's code style.
- Use descriptive variable and function names.
- Write clear and concise commit messages.
- Add tests for your changes, if possible.
- Ensure that your changes don't break existing functionality.
#### Commit message guidelines
A good commit message should describe what changed and why.
## Development
The `main` branch is regularly built and tested, but it is not guaranteed to be completely stable. Tags are created regularly from release branches to indicate new official, stable release versions of Kamal.
Kamal is written in Ruby. You should have Ruby 3.2+ installed on your machine in order to work on Kamal. If that's already setup, run `bundle` in the root directory to install all dependencies. Then you can run `bin/test` to run all tests.
1. Fork the project repository.
2. Create a new branch for your contribution.
3. Write your code or make the desired changes.
4. **Ensure that your code passes the project's minitests by running ./bin/test.**
5. Commit your changes and push them to your forked repository.
6. [Open a pull request](https://github.com/basecamp/kamal/pulls) to the main project repository with a detailed description of your changes.
## License
Kamal is released under the MIT License. By contributing to this project, you agree to license your contributions under the same license.
================================================
FILE: Dockerfile
================================================
FROM ruby:3.4-alpine
# Install docker/buildx-bin
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
# Set the working directory to /kamal
WORKDIR /kamal
# Copy the Gemfile, Gemfile.lock into the container
COPY Gemfile Gemfile.lock kamal.gemspec ./
# Required in kamal.gemspec
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb
# Install system dependencies
RUN apk add --no-cache build-base git docker-cli openssh-client-default yaml-dev \
&& gem install bundler --version=2.6.5 \
&& bundle install
# Copy the rest of our application code into the container.
# We do this after bundle install, to avoid having to run bundle
# every time we do small fixes in the source code.
COPY . .
# Install the gem locally from the project folder
RUN gem build kamal.gemspec && \
gem install ./kamal-*.gem --no-document
# Set the working directory to /workdir
WORKDIR /workdir
# Tell git it's safe to access /workdir/.git even if
# the directory is owned by a different user
RUN git config --global --add safe.directory '*'
# Set the entrypoint to run the installed binary in /workdir
# Example: docker run -it -v "$PWD:/workdir" kamal init
ENTRYPOINT ["kamal"]
================================================
FILE: Gemfile
================================================
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec
group :rubocop do
gem "rubocop-rails-omakase", require: false
end
================================================
FILE: MIT-LICENSE
================================================
Copyright (c) 2023 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Kamal: Deploy web apps anywhere
From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to seamlessly switch requests between containers. Works seamlessly across multiple servers, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.
➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands).
## Contributing to the documentation
Please help us improve Kamal's documentation on [the basecamp/kamal-site repository](https://github.com/basecamp/kamal-site).
## License
Kamal is released under the [MIT License](https://opensource.org/licenses/MIT).
================================================
FILE: bin/docs
================================================
#!/usr/bin/env ruby
require "stringio"
def usage
puts "Usage: #{$0} <kamal_site_repo>"
exit 1
end
usage if ARGV.size != 1
kamal_site_repo = ARGV[0]
if !File.directory?(kamal_site_repo)
puts "Error: #{kamal_site_repo} is not a directory"
exit 1
end
DOCS = {
"accessory" => "Accessories",
"alias" => "Aliases",
"boot" => "Booting",
"builder" => "Builders",
"configuration" => "Configuration overview",
"env" => "Environment variables",
"logging" => "Logging",
"proxy" => "Proxy",
"registry" => "Docker Registry",
"role" => "Roles",
"servers" => "Servers",
"ssh" => "SSH",
"sshkit" => "SSHKit"
}
DOCS_PATH = "lib/kamal/configuration/docs"
class DocWriter
attr_reader :from_file, :to_file, :key, :heading, :body, :output, :in_yaml
def initialize(from_file, to_dir)
@from_file = from_file
@key = File.basename(from_file, ".yml")
@to_file = File.join(to_dir, "#{linkify(DOCS[key])}.md")
@body = File.readlines(from_file)
@heading = body.shift.chomp("\n")
@output = nil
end
def write
puts "Writing #{to_file}"
generate_markdown
File.write(to_file, output.string)
end
private
def generate_markdown
@output = StringIO.new
generate_header
place = :in_section
loop do
line = body.shift&.chomp("\n")
break if line.nil?
case place
when :new_section, :in_section
if line.empty?
output.puts
place = :new_section
elsif line =~ /^ *#/
generate_line(line, heading: place == :new_section)
place = :in_section
else
output.puts
output.puts "```yaml"
output.puts line
place = :in_yaml
end
when :in_yaml, :in_empty_line_yaml
if line =~ /^ {0,4}#/
output.puts "```"
output.puts
generate_line(line, heading: place == :in_empty_line_yaml)
place = :in_section
elsif line.empty?
place = :in_empty_line_yaml
else
output.puts line
end
end
end
output.puts "```" if place == :in_yaml
end
def generate_header
output.puts "---"
output.puts "# This file has been generated from the Kamal source, do not edit directly."
output.puts "# Find the source of this file at #{DOCS_PATH}/#{key}.yml in the Kamal repository."
output.puts "title: #{heading[2..-1]}"
output.puts "---"
output.puts
output.puts heading
end
def generate_line(line, heading: false)
line = line.gsub(/^ *#\s?/, "")
if line =~ /(.*)kamal docs ([a-z]*)(.*)/
line = "#{$1}[#{DOCS[$2]}](../#{linkify(DOCS[$2])})#{$3}"
end
if line =~ /(.*)https:\/\/kamal-deploy.org([a-z\/-]*)(.*)/
line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
end
if heading
output.puts "## [#{line}](##{linkify(line)})"
else
output.puts line
end
end
def linkify(text)
if text == "Configuration overview"
"overview"
else
text.downcase.gsub(" ", "-")
end
end
def titlify(text)
text.capitalize.gsub("-", " ")
end
end
from_dir = File.join(File.dirname(__FILE__), "../#{DOCS_PATH}")
to_dir = File.join(kamal_site_repo, "docs/configuration")
Dir.glob("#{from_dir}/*") do |from_file|
DocWriter.new(from_file, to_dir).write
end
================================================
FILE: bin/kamal
================================================
#!/usr/bin/env ruby
# Prevent failures from being reported twice.
Thread.report_on_exception = false
require "kamal"
begin
Kamal::Cli::Main.start(ARGV)
rescue SSHKit::Runner::ExecuteError => e
puts " \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m"
puts e.cause.backtrace if ENV["VERBOSE"]
exit 1
rescue => e
puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
puts e.backtrace if ENV["VERBOSE"]
exit 1
end
================================================
FILE: bin/release
================================================
#!/usr/bin/env bash
VERSION=$1
printf "module Kamal\n VERSION = \"$VERSION\"\nend\n" > ./lib/kamal/version.rb
bundle
git add Gemfile.lock lib/kamal/version.rb
git commit -m "Bump version for $VERSION"
git push
git tag v$VERSION
git push --tags
gem build kamal.gemspec
gem push "kamal-$VERSION.gem" --host https://rubygems.org
rm "kamal-$VERSION.gem"
================================================
FILE: bin/test
================================================
#!/usr/bin/env ruby
$: << File.expand_path("../test", __dir__)
require "bundler/setup"
require "rails/plugin/test"
================================================
FILE: gemfiles/rails_edge.gemfile
================================================
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
git "https://github.com/rails/rails.git" do
gem "railties"
gem "activesupport"
end
gemspec path: "../"
================================================
FILE: kamal.gemspec
================================================
require_relative "lib/kamal/version"
Gem::Specification.new do |spec|
spec.name = "kamal"
spec.version = Kamal::VERSION
spec.authors = [ "David Heinemeier Hansson" ]
spec.email = "dhh@hey.com"
spec.homepage = "https://github.com/basecamp/kamal"
spec.summary = "Deploy web apps in containers to servers running Docker with zero downtime."
spec.license = "MIT"
spec.files = Dir["lib/**/*", "MIT-LICENSE", "README.md"]
spec.executables = %w[ kamal ]
spec.add_dependency "activesupport", ">= 7.0"
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
spec.add_dependency "net-ssh", "~> 7.3"
spec.add_dependency "thor", "~> 1.3"
spec.add_dependency "dotenv", "~> 3.1"
spec.add_dependency "zeitwerk", ">= 2.6.18", "< 3.0"
spec.add_dependency "ed25519", "~> 1.4"
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
spec.add_dependency "concurrent-ruby", "~> 1.2"
spec.add_dependency "base64", "~> 0.2"
spec.add_development_dependency "debug"
spec.add_development_dependency "minitest", "< 6"
spec.add_development_dependency "mocha"
spec.add_development_dependency "railties"
end
================================================
FILE: lib/kamal/cli/accessory.rb
================================================
require "active_support/core_ext/array/conversions"
require "concurrent/array"
class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
def boot(name, prepare: true)
with_lock do
if name == "all"
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
else
prepare(name) if prepare
with_accessory(name) do |accessory, hosts|
booted_hosts = Concurrent::Array.new
on(hosts) do |host|
booted_hosts << host.to_s if capture_with_info(*accessory.info(all: true, quiet: true)).strip.presence
end
if booted_hosts.any?
say "Skipping booting `#{name}` on #{booted_hosts.sort.join(", ")}, a container already exists", :yellow
hosts -= booted_hosts
end
directories(name)
upload(name)
on(hosts) do |host|
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
execute *accessory.ensure_env_directory
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
execute *accessory.run(host: host)
if accessory.running_proxy?
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
execute *accessory.deploy(target: target)
end
end
end
end
end
end
desc "upload [NAME]", "Upload accessory files to host", hide: true
def upload(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
accessory.files.each do |(local, config)|
remote = config[:host_path]
accessory.ensure_local_file_present(local)
execute *accessory.make_directory_for(remote)
upload! local, remote
execute :chmod, config[:mode], remote
execute :chown, config[:owner], remote if config[:owner]
end
end
end
end
end
desc "directories [NAME]", "Create accessory directories on host", hide: true
def directories(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
accessory.directories.each do |(local, config)|
execute *accessory.make_directory(local)
execute :chmod, config[:mode], local if config[:mode]
execute :chown, config[:owner], local if config[:owner]
end
end
end
end
end
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
def reboot(name)
with_lock do
if name == "all"
KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
else
prepare(name)
pull_image(name)
stop(name)
remove_container(name)
boot(name, prepare: false)
end
end
end
desc "start [NAME]", "Start existing accessory container on host"
def start(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
execute *accessory.start
if accessory.running_proxy?
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
execute *accessory.deploy(target: target)
end
end
end
end
end
desc "stop [NAME]", "Stop existing accessory container on host"
def stop(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
execute *accessory.stop, raise_on_non_zero_exit: false
if accessory.running_proxy?
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
execute *accessory.remove if target
end
end
end
end
end
desc "restart [NAME]", "Restart existing accessory container on host"
def restart(name)
with_lock do
stop(name)
start(name)
end
end
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
def details(name)
quiet = options[:quiet]
if name == "all"
KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
else
type = "Accessory #{name}"
with_accessory(name) do |accessory, hosts|
on(hosts) { puts_by_host host, capture_with_info(*accessory.info), type: type, quiet: quiet }
end
end
end
desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
def exec(name, *cmd)
pre_connect_if_required
cmd = Kamal::Utils.join_commands(cmd)
quiet = options[:quiet]
with_accessory(name) do |accessory, hosts|
case
when options[:interactive] && options[:reuse]
say "Launching interactive command via SSH from existing container...", :magenta
run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
when options[:interactive]
say "Launching interactive command via SSH from new container...", :magenta
on(accessory.hosts.first) { execute *KAMAL.registry.login }
run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
when options[:reuse]
say "Launching command from existing container...", :magenta
on(hosts) do |host|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd)), quiet: quiet
end
else
say "Launching command from new container...", :magenta
on(hosts) do |host|
execute *KAMAL.registry.login
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd)), quiet: quiet
end
end
end
end
desc "logs [NAME]", "Show log lines from accessory on host (use --help to show options)"
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs(name)
with_accessory(name) do |accessory, hosts|
grep = options[:grep]
grep_options = options[:grep_options]
timestamps = !options[:skip_timestamps]
if options[:follow]
run_locally do
info "Following logs on #{hosts}..."
info accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
exec accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
end
else
since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(hosts) do
puts capture_with_info(*accessory.logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
end
end
end
end
desc "pull_image [NAME]", "Pull accessory image on host", hide: true
def pull_image(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.auditor.record("Pull #{name} accessory image"), verbosity: :debug
execute *accessory.pull_image
end
end
end
end
desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def remove(name)
confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
with_lock do
if name == "all"
KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
else
remove_accessory(name)
end
end
end
end
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
def remove_container(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
execute *accessory.remove_container
end
end
end
end
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
def remove_image(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
execute *accessory.remove_image
end
end
end
end
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
def remove_service_directory(name)
with_lock do
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *accessory.remove_service_directory
end
end
end
end
desc "upgrade", "Upgrade accessories from Kamal 1.x to 2.0 (restart them in 'kamal' network)"
option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def upgrade(name)
confirming "This will restart all accessories" do
with_lock do
host_groups = options[:rolling] ? KAMAL.accessory_hosts : [ KAMAL.accessory_hosts ]
host_groups.each do |hosts|
host_list = Array(hosts).join(",")
KAMAL.with_specific_hosts(hosts) do
say "Upgrading #{name} accessories on #{host_list}...", :magenta
reboot name
say "Upgraded #{name} accessories on #{host_list}...", :magenta
end
end
end
end
end
private
def with_accessory(name)
if KAMAL.config.accessory(name)
accessory = KAMAL.accessory(name)
yield accessory, accessory_hosts(accessory)
else
error_on_missing_accessory(name)
end
end
def error_on_missing_accessory(name)
options = KAMAL.accessory_names.presence
error \
"No accessory by the name of '#{name}'" +
(options ? " (options: #{options.to_sentence})" : "")
end
def accessory_hosts(accessory)
KAMAL.accessory_hosts & accessory.hosts
end
def remove_accessory(name)
stop(name)
remove_container(name)
remove_image(name)
remove_service_directory(name)
end
def prepare(name)
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.registry.login(registry_config: accessory.registry)
execute *KAMAL.docker.create_network
rescue SSHKit::Command::Failed => e
raise unless e.message.include?("already exists")
end
end
end
end
================================================
FILE: lib/kamal/cli/alias/command.rb
================================================
class Kamal::Cli::Alias::Command < Thor::DynamicCommand
def run(instance, args = [])
if (command = KAMAL.resolve_alias(name))
KAMAL.reset
Kamal::Cli::Main.start(Shellwords.split(command) + ARGV[1..-1])
else
super
end
end
end
================================================
FILE: lib/kamal/cli/app/assets.rb
================================================
class Kamal::Cli::App::Assets
attr_reader :host, :role, :sshkit
delegate :execute, :capture_with_info, :info, to: :sshkit
delegate :assets?, to: :role
def initialize(host, role, sshkit)
@host = host
@role = role
@sshkit = sshkit
end
def run
if assets?
execute *app.extract_assets
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
execute *app.sync_asset_volumes(old_version: old_version)
end
end
private
def app
@app ||= KAMAL.app(role: role, host: host)
end
end
================================================
FILE: lib/kamal/cli/app/boot.rb
================================================
class Kamal::Cli::App::Boot
attr_reader :host, :role, :version, :barrier, :sshkit
delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, :upload!, to: :sshkit
delegate :assets?, :running_proxy?, to: :role
def initialize(host, role, sshkit, version, barrier)
@host = host
@role = role
@version = version
@barrier = barrier
@sshkit = sshkit
end
def run
old_version = old_version_renamed_if_clashing
wait_at_barrier if queuer?
begin
start_new_version
rescue => e
close_barrier if gatekeeper?
stop_new_version
raise
end
release_barrier if gatekeeper?
if old_version
stop_old_version(old_version)
end
end
private
def old_version_renamed_if_clashing
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
renamed_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
info "Renaming container #{version} to #{renamed_version} as already deployed on #{host}"
audit("Renaming container #{version} to #{renamed_version}")
execute *app.rename_container(version: version, new_version: renamed_version)
end
capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence
end
def start_new_version
audit "Booted app version #{version}"
hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
execute *app.ensure_env_directory
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
execute *app.run(hostname: hostname)
if running_proxy?
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
execute *app.deploy(target: endpoint)
else
Kamal::Cli::Healthcheck::Poller.wait_for_healthy { capture_with_info(*app.status(version: version)) }
end
rescue => e
error "Failed to boot #{role} on #{host}"
raise e
end
def stop_new_version
execute *app.stop(version: version), raise_on_non_zero_exit: false
end
def stop_old_version(version)
execute *app.stop(version: version), raise_on_non_zero_exit: false
execute *app.clean_up_assets if assets?
execute *app.clean_up_error_pages if KAMAL.config.error_pages_path
end
def release_barrier
if barrier.open
info "First #{KAMAL.primary_role} container is healthy on #{host}, booting any other roles"
end
end
def wait_at_barrier
info "Waiting for the first healthy #{KAMAL.primary_role} container before booting #{role} on #{host}..."
barrier.wait
info "First #{KAMAL.primary_role} container is healthy, booting #{role} on #{host}..."
rescue Kamal::Cli::Healthcheck::Error
info "First #{KAMAL.primary_role} container is unhealthy, not booting #{role} on #{host}"
raise
end
def close_barrier
if barrier.close
info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
begin
error capture_with_info(*app.logs(container_id: app.container_id_for_version(version)))
error capture_with_info(*app.container_health_log(version: version))
rescue SSHKit::Command::Failed
error "Could not fetch logs for #{version}"
end
end
end
def barrier_role?
role == KAMAL.primary_role
end
def app
@app ||= KAMAL.app(role: role, host: host)
end
def auditor
@auditor = KAMAL.auditor(role: role)
end
def audit(message)
execute *auditor.record(message), verbosity: :debug
end
def gatekeeper?
barrier && barrier_role?
end
def queuer?
barrier && !barrier_role?
end
end
================================================
FILE: lib/kamal/cli/app/error_pages.rb
================================================
class Kamal::Cli::App::ErrorPages
ERROR_PAGES_GLOB = "{4??.html,5??.html}"
attr_reader :host, :sshkit
delegate :upload!, :execute, to: :sshkit
def initialize(host, sshkit)
@host = host
@sshkit = sshkit
end
def run
if KAMAL.config.error_pages_path
with_error_pages_tmpdir do |local_error_pages_dir|
execute *KAMAL.app.create_error_pages_directory
upload! local_error_pages_dir, KAMAL.config.proxy_boot.error_pages_directory, mode: "0700", recursive: true
end
end
end
private
def with_error_pages_tmpdir
Dir.mktmpdir("kamal-error-pages") do |tmpdir|
error_pages_dir = File.join(tmpdir, KAMAL.config.version)
FileUtils.mkdir(error_pages_dir)
if (files = Dir[File.join(KAMAL.config.error_pages_path, ERROR_PAGES_GLOB)]).any?
FileUtils.cp(files, error_pages_dir)
yield error_pages_dir
end
end
end
end
================================================
FILE: lib/kamal/cli/app/ssl_certificates.rb
================================================
class Kamal::Cli::App::SslCertificates
attr_reader :host, :role, :sshkit
delegate :execute, :info, :upload!, to: :sshkit
def initialize(host, role, sshkit)
@host = host
@role = role
@sshkit = sshkit
end
def run
if role.running_proxy? && role.proxy.custom_ssl_certificate?
info "Writing SSL certificates for #{role.name} on #{host}"
execute *app.create_ssl_directory
if cert_content = role.proxy.certificate_pem_content
upload!(StringIO.new(cert_content), role.proxy.host_tls_cert, mode: "0644")
end
if key_content = role.proxy.private_key_pem_content
upload!(StringIO.new(key_content), role.proxy.host_tls_key, mode: "0644")
end
end
end
private
def app
@app ||= KAMAL.app(role: role, host: host)
end
end
================================================
FILE: lib/kamal/cli/app.rb
================================================
class Kamal::Cli::App < Kamal::Cli::Base
desc "boot", "Boot app on servers (or reboot app if already running)"
def boot
with_lock do
say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(version_or_latest) do |version|
say "Start container with version #{version} (or reboot if already running)...", :magenta
# Assets are prepared in a separate step to ensure they are on all hosts before booting
on(KAMAL.app_hosts) do
Kamal::Cli::App::ErrorPages.new(host, self).run
KAMAL.roles_on(host).each do |role|
Kamal::Cli::App::Assets.new(host, role, self).run
Kamal::Cli::App::SslCertificates.new(host, role, self).run
end
end
# Primary hosts and roles are returned first, so they can open the barrier
barrier = Kamal::Cli::Healthcheck::Barrier.new
host_boot_groups.each do |hosts|
host_list = Array(hosts).join(",")
run_hook "pre-app-boot", hosts: host_list
on_roles(KAMAL.roles, hosts: hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
Kamal::Cli::App::Boot.new(host, role, self, version, barrier).run
end
run_hook "post-app-boot", hosts: host_list
sleep KAMAL.config.boot.wait if KAMAL.config.boot.wait
end
# Tag once the app booted on all hosts
on(KAMAL.app_hosts) do |host|
execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
execute *KAMAL.app.tag_latest_image
end
end
end
end
desc "start", "Start existing app container on servers"
def start
with_lock do
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
app = KAMAL.app(role: role, host: host)
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
execute *app.start, raise_on_non_zero_exit: false
if role.running_proxy?
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
execute *app.deploy(target: endpoint)
end
end
end
end
desc "stop", "Stop app container on servers"
def stop
with_lock do
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
app = KAMAL.app(role: role, host: host)
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
if role.running_proxy?
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
if endpoint.present?
execute *app.remove, raise_on_non_zero_exit: false
end
end
execute *app.stop, raise_on_non_zero_exit: false
end
end
end
# FIXME: Drop in favor of just containers?
desc "details", "Show details about app containers"
def details
quiet = options[:quiet]
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info), quiet: quiet
end
end
desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)"
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
def exec(*cmd)
pre_connect_if_required
if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
end
if cmd.empty?
raise ArgumentError, "No command provided. You must specify a command to execute."
end
cmd = Kamal::Utils.join_commands(cmd)
env = options[:env]
detach = options[:detach]
quiet = options[:quiet]
case
when options[:interactive] && options[:reuse]
say "Get current version of running container...", :magenta unless options[:version]
using_version(options[:version] || current_running_version) do |version|
say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
end
when options[:interactive]
say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(version_or_latest) do |version|
say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
on(KAMAL.primary_host) { execute *KAMAL.registry.login }
run_locally do
exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
end
end
when options[:reuse]
say "Get current version of running container...", :magenta unless options[:version]
using_version(options[:version] || current_running_version) do |version|
say "Launching command with version #{version} from existing container...", :magenta
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env)), quiet: quiet
end
end
else
say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(version_or_latest) do |version|
say "Launching command with version #{version} from new container...", :magenta
on(KAMAL.app_hosts) { execute *KAMAL.registry.login }
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach)), quiet: quiet
end
end
end
end
desc "containers", "Show app containers on servers"
def containers
quiet = options[:quiet]
on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers), quiet: quiet }
end
desc "stale_containers", "Detect app stale containers"
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
def stale_containers
quiet = options[:quiet]
stop = options[:stop]
with_lock_if_stopping do
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
app = KAMAL.app(role: role, host: host)
versions = capture_with_info(*app.list_versions, raise_on_non_zero_exit: false).split("\n")
versions -= [ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip ]
versions.each do |version|
if stop
puts_by_host host, "Stopping stale container for role #{role} with version #{version}", quiet: quiet
execute *app.stop(version: version), raise_on_non_zero_exit: false
else
puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)", quiet: quiet
end
end
end
end
end
desc "images", "Show app images on servers"
def images
quiet = options[:quiet]
on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images), quiet: quiet }
end
desc "logs", "Show log lines from app on servers (use --help to show options)"
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
option :container_id, desc: "Docker container ID to fetch logs"
def logs
# FIXME: Catch when app containers aren't running
grep = options[:grep]
grep_options = options[:grep_options]
since = options[:since]
container_id = options[:container_id]
timestamps = !options[:skip_timestamps]
quiet = options[:quiet]
if options[:follow]
lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
run_locally do
info "Following logs on #{KAMAL.primary_host}..."
KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
role = KAMAL.roles_on(KAMAL.primary_host).first
app = KAMAL.app(role: role, host: host)
info app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
exec app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
end
else
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
begin
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(container_id: container_id, timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options)), quiet: quiet
rescue SSHKit::Command::Failed
puts_by_host host, "Nothing found", quiet: quiet
end
end
end
end
desc "remove", "Remove app containers and images from servers"
def remove
with_lock do
stop
remove_containers
remove_images
remove_app_directories
end
end
desc "live", "Set the app to live mode"
def live
with_lock do
on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
execute *KAMAL.app(role: role, host: host).live if role.running_proxy?
end
end
end
desc "maintenance", "Set the app to maintenance mode"
option :drain_timeout, type: :numeric, desc: "How long to allow in-flight requests to complete (defaults to drain_timeout from config)"
option :message, type: :string, desc: "Message to display to clients while stopped"
def maintenance
maintenance_options = { drain_timeout: options[:drain_timeout] || KAMAL.config.drain_timeout, message: options[:message] }
with_lock do
on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
execute *KAMAL.app(role: role, host: host).maintenance(**maintenance_options) if role.running_proxy?
end
end
end
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
def remove_container(version)
with_lock do
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
execute *KAMAL.app(role: role, host: host).remove_container(version: version)
end
end
end
desc "remove_containers", "Remove all app containers from servers", hide: true
def remove_containers
with_lock do
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
execute *KAMAL.app(role: role, host: host).remove_containers
end
end
end
desc "remove_images", "Remove all app images from servers", hide: true
def remove_images
with_lock do
on(hosts_removing_all_roles) do
execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
execute *KAMAL.app.remove_images
end
end
end
desc "remove_app_directories", "Remove the app directories from servers", hide: true
def remove_app_directories
with_lock do
on(hosts_removing_all_roles) do |host|
execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}"), verbosity: :debug
execute *KAMAL.app.remove_proxy_app_directory, raise_on_non_zero_exit: false
end
end
end
desc "version", "Show app version currently running on servers"
def version
quiet = options[:quiet]
on(KAMAL.app_hosts) do |host|
role = KAMAL.roles_on(host).first
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip, quiet: quiet
end
end
private
def hosts_removing_all_roles
KAMAL.app_hosts.select { |host| KAMAL.roles_on(host).map(&:name).sort == KAMAL.config.host_roles(host.to_s).map(&:name).sort }
end
def using_version(new_version)
if new_version
begin
old_version = KAMAL.config.version
KAMAL.config.version = new_version
yield new_version
ensure
KAMAL.config.version = old_version
end
else
yield KAMAL.config.version
end
end
def current_running_version(host: KAMAL.primary_host)
version = nil
on(host) do
role = KAMAL.roles_on(host).first
version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
end
version.presence
end
def version_or_latest
options[:version] || KAMAL.config.latest_tag
end
def with_lock_if_stopping
if options[:stop]
with_lock { yield }
else
yield
end
end
def host_boot_groups
KAMAL.config.boot.limit ? KAMAL.app_hosts.each_slice(KAMAL.config.boot.limit).to_a : [ KAMAL.app_hosts ]
end
end
================================================
FILE: lib/kamal/cli/base.rb
================================================
require "thor"
require "kamal/sshkit_with_ext"
module Kamal::Cli
class Base < Thor
include SSHKit::DSL
VERBOSITY = { verbose: :debug, quiet: :error }.freeze
def self.exit_on_failure?() true end
def self.dynamic_command_class() Kamal::Cli::Alias::Command end
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
class_option :version, desc: "Run commands against a specific app version"
class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)"
class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)"
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
def initialize(args = [], local_options = {}, config = {})
if config[:current_command].is_a?(Kamal::Cli::Alias::Command)
# When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
# For our purposes, it means the arguments are passed in args rather than local_options.
super([], args, config)
else
super
end
initialize_commander unless KAMAL.configured?
end
private
def options_with_subcommand_class_options
options.merge(@_initializer.last[:class_options] || {})
end
def initialize_commander
KAMAL.tap do |commander|
if options[:verbose]
ENV["VERBOSE"] = "1" # For backtraces via cli/start
commander.verbosity = VERBOSITY[:verbose]
end
if options[:quiet]
commander.verbosity = VERBOSITY[:quiet]
end
commander.configure \
config_file: Pathname.new(File.expand_path(options[:config_file])),
destination: options[:destination],
version: options[:version]
commander.specific_hosts = options[:hosts]&.split(",")
commander.specific_roles = options[:roles]&.split(",")
commander.specific_primary! if options[:primary]
end
end
def print_runtime
started_at = Time.now
yield
Time.now - started_at
ensure
runtime = Time.now - started_at
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
end
def with_lock
if KAMAL.holding_lock?
yield
else
acquire_lock
begin
yield
rescue
begin
release_lock
rescue => e
say "Error releasing the deploy lock: #{e.message}", :red
end
raise
end
release_lock
end
end
def confirming(question)
return yield if options[:confirmed]
if ask(question, limited_to: %w[ y N ], default: "N") == "y"
yield
else
say "Aborted", :red
end
end
def acquire_lock
ensure_run_directory
raise_if_locked do
say "Acquiring the deploy lock...", :magenta
on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
end
KAMAL.holding_lock = true
end
def release_lock
say "Releasing the deploy lock...", :magenta
on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
KAMAL.holding_lock = false
end
def raise_if_locked
yield
rescue SSHKit::Runner::ExecuteError => e
if e.message =~ /cannot create directory/
say "Deploy lock already in place!", :red
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
else
raise e
end
end
def run_hook(hook, **extra_details)
if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
details = {
hosts: KAMAL.hosts.join(","),
roles: KAMAL.specific_roles&.join(","),
lock: KAMAL.holding_lock?.to_s,
command: command,
subcommand: subcommand
}.compact
hooks_output = KAMAL.config.hooks_output_for(hook)
# CLI flags override config: -q hides all, -v shows all
# Config setting :verbose forces output, :quiet forces silence
hook_verbosity = if KAMAL.verbosity == :info && hooks_output
VERBOSITY.fetch(hooks_output)
else
KAMAL.verbosity
end
with_env KAMAL.hook.env(**details, **extra_details) do
KAMAL.with_verbosity(hook_verbosity) do
run_locally do
execute *KAMAL.hook.run(hook)
end
end
rescue SSHKit::Command::Failed => e
raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
end
end
end
def on(*args, &block)
pre_connect_if_required
super
end
def pre_connect_if_required
if !KAMAL.connected?
run_hook "pre-connect", secrets: true unless options[:skip_hooks]
KAMAL.connected = true
end
end
def command
@kamal_command ||= begin
invocation_class, invocation_commands = *first_invocation
if invocation_class == Kamal::Cli::Main
invocation_commands[0]
else
Kamal::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
end
end
end
def subcommand
@kamal_subcommand ||= begin
invocation_class, invocation_commands = *first_invocation
invocation_commands[0] if invocation_class != Kamal::Cli::Main
end
end
def first_invocation
instance_variable_get("@_invocations").first
end
def reset_invocation(cli_class)
instance_variable_get("@_invocations")[cli_class].pop
end
def ensure_run_directory
on(KAMAL.hosts) do
execute(*KAMAL.server.ensure_run_directory)
end
end
def with_env(env)
current_env = ENV.to_h.dup
ENV.update(env)
yield
ensure
ENV.clear
ENV.update(current_env)
end
def ensure_docker_installed
run_locally do
begin
execute *KAMAL.builder.ensure_docker_installed
rescue SSHKit::Command::Failed => e
error = e.message =~ /command not found/ ?
"Docker is not installed locally" :
"Docker buildx plugin is not installed locally"
raise DependencyError, error
end
end
end
end
end
================================================
FILE: lib/kamal/cli/build/clone.rb
================================================
class Kamal::Cli::Build::Clone
attr_reader :sshkit
delegate :info, :error, :execute, :capture_with_info, to: :sshkit
def initialize(sshkit)
@sshkit = sshkit
end
def prepare
begin
clone_repo
rescue SSHKit::Command::Failed => e
if e.message =~ /already exists and is not an empty directory/
reset
else
raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
end
end
validate!
rescue Kamal::Cli::Build::BuildError => e
error "Error preparing clone: #{e.message}, deleting and retrying..."
FileUtils.rm_rf KAMAL.config.builder.clone_directory
clone_repo
validate!
end
private
def clone_repo
info "Cloning repo into build directory `#{KAMAL.config.builder.build_directory}`..."
FileUtils.mkdir_p KAMAL.config.builder.clone_directory
execute *KAMAL.builder.clone
end
def reset
info "Resetting local clone as `#{KAMAL.config.builder.build_directory}` already exists..."
KAMAL.builder.clone_reset_steps.each { |step| execute *step }
rescue SSHKit::Command::Failed => e
raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
end
def validate!
status = capture_with_info(*KAMAL.builder.clone_status).strip
unless status.empty?
raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is dirty, #{status}"
end
revision = capture_with_info(*KAMAL.builder.clone_revision).strip
if revision != Kamal::Git.revision
raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is not on the correct revision, expected `#{Kamal::Git.revision}` but got `#{revision}`"
end
rescue SSHKit::Command::Failed => e
raise Kamal::Cli::Build::BuildError, "Failed to validate clone: #{e.message}"
end
end
================================================
FILE: lib/kamal/cli/build/port_forwarding.rb
================================================
require "concurrent/atomic/count_down_latch"
class Kamal::Cli::Build::PortForwarding
attr_reader :hosts, :port, :ssh_options
def initialize(hosts, port, **ssh_options)
@hosts = hosts
@port = port
@ssh_options = ssh_options
end
def forward
@done = false
forward_ports
yield
ensure
stop
end
private
def stop
@done = true
@threads.to_a.each(&:join)
end
def forward_ports
ready = Concurrent::CountDownLatch.new(hosts.size)
@threads = hosts.map do |host|
Thread.new do
begin
Net::SSH.start(host, ssh_options[:user], **ssh_options.except(:user)) do |ssh|
ssh.forward.remote(port, "localhost", port, "127.0.0.1") do |remote_port, bind_address|
if remote_port == :error
raise "Failed to establish port forward on #{host}"
else
ready.count_down
end
end
ssh.loop(0.1) do
if @done
ssh.forward.cancel_remote(port, "127.0.0.1")
break
else
true
end
end
end
rescue Exception => e
error "Error setting up port forwarding to #{host}: #{e.class}: #{e.message}"
error e.backtrace.join("\n")
raise
end
end
end
raise "Timed out waiting for port forwarding to be established" unless ready.wait(30)
end
def error(message)
SSHKit.config.output.error(message)
end
end
================================================
FILE: lib/kamal/cli/build.rb
================================================
class Kamal::Cli::Build < Kamal::Cli::Base
class BuildError < StandardError; end
desc "deliver", "Build app and push app image to registry then pull image on servers"
def deliver
invoke :push
invoke :pull
end
desc "push", "Build and push app image to registry"
option :output, type: :string, default: "registry", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
def push
cli = self
# Ensure pre-connect hooks run before the build, they may be needed for a remote builder
# or the pre-build hooks.
pre_connect_if_required
ensure_docker_installed
login_to_registry_locally if KAMAL.builder.login_to_registry_locally?
run_hook "pre-build"
uncommitted_changes = Kamal::Git.uncommitted_changes
if KAMAL.config.builder.git_clone?
if uncommitted_changes.present?
say "Building from a local git clone, so ignoring these uncommitted changes:\n #{uncommitted_changes}", :yellow
end
run_locally do
Clone.new(self).prepare
end
elsif uncommitted_changes.present?
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
end
forward_local_registry_port_for_remote_builder do
with_env(KAMAL.config.builder.secrets) do
run_locally do
begin
execute *KAMAL.builder.inspect_builder
rescue SSHKit::Command::Failed => e
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
warn "Missing compatible builder, so creating a new one first"
begin
cli.remove
rescue SSHKit::Command::Failed
raise unless e.message =~ /(context not found|no builder|does not exist)/
end
cli.create
else
raise
end
end
# Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push(cli.options[:output], no_cache: cli.options[:no_cache])
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.builder.push_env }
end
end
end
end
end
desc "pull", "Pull app image from registry onto servers"
def pull
login_to_registry_remotely unless KAMAL.registry.local?
forward_local_registry_port(KAMAL.hosts, **KAMAL.config.ssh.options) do
if (first_hosts = mirror_hosts).any?
# Pull on a single host per mirror first to seed them
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
pull_on_hosts(first_hosts)
say "Pulling image on remaining hosts...", :magenta
pull_on_hosts(KAMAL.app_hosts - first_hosts)
else
pull_on_hosts(KAMAL.app_hosts)
end
end
end
desc "create", "Create a build setup"
def create
if (remote_host = KAMAL.config.builder.remote)
connect_to_remote_host(remote_host)
end
run_locally do
begin
debug "Using builder: #{KAMAL.builder.name}"
execute *KAMAL.builder.create
rescue SSHKit::Command::Failed => e
if e.message =~ /stderr=(.*)/
error "Couldn't create remote builder: #{$1}"
false
else
raise
end
end
end
end
desc "remove", "Remove build setup"
def remove
run_locally do
debug "Using builder: #{KAMAL.builder.name}"
execute *KAMAL.builder.remove
end
end
desc "details", "Show build setup"
def details
run_locally do
puts "Builder: #{KAMAL.builder.name}"
puts capture(*KAMAL.builder.info)
end
end
desc "dev", "Build using the working directory, tag it as dirty, and push to local image store."
option :output, type: :string, default: "docker", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
def dev
cli = self
ensure_docker_installed
docker_included_files = Set.new(Kamal::Docker.included_files)
git_uncommitted_files = Set.new(Kamal::Git.uncommitted_files)
git_untracked_files = Set.new(Kamal::Git.untracked_files)
docker_uncommitted_files = docker_included_files & git_uncommitted_files
if docker_uncommitted_files.any?
say "WARNING: Files with uncommitted changes will be present in the dev container:", :yellow
docker_uncommitted_files.sort.each { |f| say " #{f}", :yellow }
say
end
docker_untracked_files = docker_included_files & git_untracked_files
if docker_untracked_files.any?
say "WARNING: Untracked files will be present in the dev container:", :yellow
docker_untracked_files.sort.each { |f| say " #{f}", :yellow }
say
end
with_env(KAMAL.config.builder.secrets) do
run_locally do
build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true, no_cache: cli.options[:no_cache])
KAMAL.with_verbosity(:debug) do
execute(*build)
end
end
end
end
private
def connect_to_remote_host(remote_host)
remote_uri = URI.parse(remote_host)
if remote_uri.scheme == "ssh"
host = SSHKit::Host.new(
hostname: remote_uri.host,
ssh_options: { user: remote_uri.user, port: remote_uri.port }.compact
)
on(host, options) do
execute "true"
end
end
end
def mirror_hosts
if KAMAL.app_hosts.many?
mirror_hosts = Concurrent::Hash.new
on(KAMAL.app_hosts) do |host|
first_mirror = capture_with_info(*KAMAL.builder.first_mirror).strip.presence
mirror_hosts[first_mirror] ||= host.to_s if first_mirror
rescue SSHKit::Command::Failed => e
raise unless e.message =~ /error calling index: reflect: slice index out of range/
end
mirror_hosts.values
else
[]
end
end
def pull_on_hosts(hosts)
on(hosts) do
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
execute *KAMAL.builder.pull
execute *KAMAL.builder.validate_image
end
end
def login_to_registry_locally
run_locally do
if KAMAL.registry.local?
execute *KAMAL.registry.setup
else
execute *KAMAL.registry.login
end
end
end
def login_to_registry_remotely
on(KAMAL.app_hosts) do
execute *KAMAL.registry.login
end
end
def forward_local_registry_port_for_remote_builder(&block)
if KAMAL.builder.remote?
remote_uri = URI(KAMAL.config.builder.remote)
forward_local_registry_port([ remote_uri.host ], **remote_builder_ssh_options(remote_uri), &block)
else
yield
end
end
def forward_local_registry_port(hosts, **ssh_options, &block)
if KAMAL.config.registry.local?
say "Setting up local registry port forwarding to #{hosts.join(', ')}..."
PortForwarding.new(hosts, KAMAL.config.registry.local_port, **ssh_options).forward(&block)
else
yield
end
end
def remote_builder_ssh_options(remote_uri)
{ user: remote_uri.user,
port: remote_uri.port,
keepalive: KAMAL.config.ssh.options[:keepalive],
keepalive_interval: KAMAL.config.ssh.options[:keepalive_interval],
logger: KAMAL.config.ssh.options[:logger]
}.compact
end
end
================================================
FILE: lib/kamal/cli/healthcheck/barrier.rb
================================================
require "concurrent/ivar"
class Kamal::Cli::Healthcheck::Barrier
def initialize
@ivar = Concurrent::IVar.new
end
def close
set(false)
end
def open
set(true)
end
def wait
unless opened?
raise Kamal::Cli::Healthcheck::Error.new("Halted at barrier")
end
end
private
def opened?
@ivar.value
end
def set(value)
@ivar.set(value)
true
rescue Concurrent::MultipleAssignmentError
false
end
end
================================================
FILE: lib/kamal/cli/healthcheck/error.rb
================================================
class Kamal::Cli::Healthcheck::Error < StandardError
end
================================================
FILE: lib/kamal/cli/healthcheck/poller.rb
================================================
module Kamal::Cli::Healthcheck::Poller
extend self
def wait_for_healthy(&block)
attempt = 1
timeout_at = Time.now + KAMAL.config.deploy_timeout
readiness_delay = KAMAL.config.readiness_delay
begin
status = block.call
if status == "running"
# Wait for the readiness delay and confirm it is still running
if readiness_delay > 0
info "Container is running, waiting for readiness delay of #{readiness_delay} seconds"
sleep readiness_delay
status = block.call
end
end
unless %w[ running healthy ].include?(status)
raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.deploy_timeout} seconds (#{status})"
end
rescue Kamal::Cli::Healthcheck::Error => e
time_left = timeout_at - Time.now
if time_left > 0
sleep [ attempt, time_left ].min
attempt += 1
retry
else
raise
end
end
info "Container is healthy!"
end
private
def info(message)
SSHKit.config.output.info(message)
end
end
================================================
FILE: lib/kamal/cli/lock.rb
================================================
class Kamal::Cli::Lock < Kamal::Cli::Base
desc "status", "Report lock status"
def status
handle_missing_lock do
on(KAMAL.primary_host) do
puts capture_with_debug(*KAMAL.lock.status)
end
end
end
desc "acquire", "Acquire the deploy lock"
option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
def acquire
message = options[:message]
ensure_run_directory
raise_if_locked do
on(KAMAL.primary_host) do
execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
end
say "Acquired the deploy lock"
end
end
desc "release", "Release the deploy lock"
def release
handle_missing_lock do
on(KAMAL.primary_host) do
execute *KAMAL.lock.release, verbosity: :debug
end
say "Released the deploy lock"
end
end
private
def handle_missing_lock
yield
rescue SSHKit::Runner::ExecuteError => e
if e.message =~ /No such file or directory/
say "There is no deploy lock"
else
raise
end
end
end
================================================
FILE: lib/kamal/cli/main.rb
================================================
class Kamal::Cli::Main < Kamal::Cli::Base
desc "setup", "Setup all accessories, push the env, and deploy app to servers"
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
def setup
print_runtime do
with_lock do
invoke_options = deploy_options
say "Ensure Docker is installed...", :magenta
invoke "kamal:cli:server:bootstrap", [], invoke_options
deploy(boot_accessories: true)
end
end
end
desc "deploy", "Deploy app to servers"
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
def deploy(boot_accessories: false)
runtime = print_runtime do
invoke_options = deploy_options
if options[:skip_push]
say "Pull app image...", :magenta
invoke "kamal:cli:build:pull", [], invoke_options
else
say "Build and push app image...", :magenta
invoke "kamal:cli:build:deliver", [], invoke_options
end
with_lock do
run_hook "pre-deploy", secrets: true
say "Ensure kamal-proxy is running...", :magenta
invoke "kamal:cli:proxy:boot", [], invoke_options
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options if boot_accessories
say "Detect stale containers...", :magenta
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
invoke "kamal:cli:app:boot", [], invoke_options
say "Prune old containers and images...", :magenta
invoke "kamal:cli:prune:all", [], invoke_options
end
end
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy and pruning"
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
def redeploy
runtime = print_runtime do
invoke_options = deploy_options
if options[:skip_push]
say "Pull app image...", :magenta
invoke "kamal:cli:build:pull", [], invoke_options
else
say "Build and push app image...", :magenta
invoke "kamal:cli:build:deliver", [], invoke_options
end
with_lock do
run_hook "pre-deploy", secrets: true
say "Detect stale containers...", :magenta
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
invoke "kamal:cli:app:boot", [], invoke_options
end
end
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end
desc "rollback [VERSION]", "Rollback app to VERSION"
def rollback(version)
rolled_back = false
runtime = print_runtime do
with_lock do
invoke_options = deploy_options
KAMAL.config.version = version
old_version = nil
if container_available?(version)
run_hook "pre-deploy", secrets: true
invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
rolled_back = true
else
say "The app version '#{version}' is not available as a container (use 'kamal app containers' for available versions)", :red
end
end
end
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
end
desc "details", "Show details about all containers"
def details
invoke "kamal:cli:proxy:details"
invoke "kamal:cli:app:details"
invoke "kamal:cli:accessory:details", [ "all" ]
end
desc "audit", "Show audit log from servers"
def audit
quiet = options[:quiet]
on(KAMAL.hosts) do |host|
puts_by_host host, capture_with_info(*KAMAL.auditor.reveal), quiet: quiet
end
end
desc "config", "Show combined config (including secrets!)"
def config
run_locally do
puts Kamal::Utils.redacted(KAMAL.config.to_h).to_yaml
end
end
desc "docs [SECTION]", "Show Kamal configuration documentation"
def docs(section = nil)
case section
when NilClass
puts Kamal::Configuration.validation_doc
else
puts Kamal::Configuration.const_get(section.titlecase.to_sym).validation_doc
end
rescue NameError
puts "No documentation found for #{section}"
end
desc "init", "Create config stub in config/deploy.yml and secrets stub in .kamal"
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
def init
require "fileutils"
if (deploy_file = Pathname.new(File.expand_path("config/deploy.yml"))).exist?
puts "Config file already exists in config/deploy.yml (remove first to create a new one)"
else
FileUtils.mkdir_p deploy_file.dirname
FileUtils.cp_r Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file
puts "Created configuration file in config/deploy.yml"
end
unless (secrets_file = Pathname.new(File.expand_path(".kamal/secrets"))).exist?
FileUtils.mkdir_p secrets_file.dirname
FileUtils.cp_r Pathname.new(File.expand_path("templates/secrets", __dir__)), secrets_file
puts "Created .kamal/secrets file"
end
unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
hooks_dir.mkpath
Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
FileUtils.cp sample_hook, hooks_dir, preserve: true
end
puts "Created sample hooks in .kamal/hooks"
end
if options[:bundle]
if (binstub = Pathname.new(File.expand_path("bin/kamal"))).exist?
puts "Binstub already exists in bin/kamal (remove first to create a new one)"
else
puts "Adding Kamal to Gemfile and bundle..."
run_locally do
execute :bundle, :add, :kamal
execute :bundle, :binstubs, :kamal
end
puts "Created binstub file in bin/kamal"
end
end
end
desc "remove", "Remove kamal-proxy, app, accessories, and registry session from servers"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def remove
confirming "This will remove all containers and images. Are you sure?" do
with_lock do
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
invoke "kamal:cli:accessory:remove", [ "all" ], options
invoke "kamal:cli:registry:remove", [], options.without(:confirmed).merge(skip_local: true)
end
end
end
desc "upgrade", "Upgrade from Kamal 1.x to 2.0"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
def upgrade
confirming "This will replace Traefik with kamal-proxy and restart all accessories" do
with_lock do
if options[:rolling]
KAMAL.hosts.each do |host|
KAMAL.with_specific_hosts(host) do
say "Upgrading #{host}...", :magenta
if KAMAL.app_hosts.include?(host)
invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true, rolling: false)
reset_invocation(Kamal::Cli::Proxy)
end
if KAMAL.accessory_hosts.include?(host)
invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true, rolling: false)
reset_invocation(Kamal::Cli::Accessory)
end
say "Upgraded #{host}", :magenta
end
end
else
say "Upgrading all hosts...", :magenta
invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true)
invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true)
say "Upgraded all hosts", :magenta
end
end
end
end
desc "version", "Show Kamal version"
def version
puts Kamal::VERSION
end
desc "accessory", "Manage accessories (db/redis/search)"
subcommand "accessory", Kamal::Cli::Accessory
desc "app", "Manage application"
subcommand "app", Kamal::Cli::App
desc "build", "Build application image"
subcommand "build", Kamal::Cli::Build
desc "lock", "Manage the deploy lock"
subcommand "lock", Kamal::Cli::Lock
desc "proxy", "Manage kamal-proxy"
subcommand "proxy", Kamal::Cli::Proxy
desc "prune", "Prune old application images and containers"
subcommand "prune", Kamal::Cli::Prune
desc "registry", "Login and -out of the image registry"
subcommand "registry", Kamal::Cli::Registry
desc "secrets", "Helpers for extracting secrets"
subcommand "secrets", Kamal::Cli::Secrets
desc "server", "Bootstrap servers with curl and Docker"
subcommand "server", Kamal::Cli::Server
private
def container_available?(version)
begin
on(KAMAL.app_hosts) do
KAMAL.roles_on(host).each do |role|
container_id = capture_with_info(*KAMAL.app(role: role, host: host).container_id_for_version(version))
raise "Container not found" unless container_id.present?
end
end
rescue SSHKit::Runner::ExecuteError, SSHKit::Runner::MultipleExecuteError => e
if e.message =~ /Container not found/
say "Error looking for container version #{version}: #{e.message}"
return false
else
raise
end
end
true
end
def deploy_options
base_options = options.without("skip_push")
base_options = base_options.except("no_cache") unless base_options["no_cache"]
{ "version" => KAMAL.config.version }.merge(base_options)
end
end
================================================
FILE: lib/kamal/cli/proxy.rb
================================================
class Kamal::Cli::Proxy < Kamal::Cli::Base
desc "boot", "Boot proxy on servers"
def boot
with_lock do
on(KAMAL.hosts) do |host|
execute *KAMAL.docker.create_network
rescue SSHKit::Command::Failed => e
raise unless e.message.include?("already exists")
end
on(KAMAL.proxy_hosts) do |host|
execute *KAMAL.registry.login
version = capture_with_info(*KAMAL.proxy(host).version).strip.presence
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Run::MINIMUM_VERSION)
raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION}"
end
execute *KAMAL.proxy(host).ensure_apps_config_directory
execute *KAMAL.proxy(host).start_or_run
end
end
end
desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
option :publish_host_ip, type: :string, repeatable: true, default: nil, desc: "Host IP address to bind HTTP/HTTPS traffic to. Defaults to all interfaces"
option :http_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT, desc: "HTTP port to publish on the host"
option :https_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT, desc: "HTTPS port to publish on the host"
option :log_max_size, type: :string, default: Kamal::Configuration::Proxy::Run::DEFAULT_LOG_MAX_SIZE, desc: "Max size of proxy logs"
option :registry, type: :string, default: nil, desc: "Registry to use for the proxy image"
option :repository, type: :string, default: nil, desc: "Repository for the proxy image"
option :image_version, type: :string, default: nil, desc: "Version of the proxy to run"
option :metrics_port, type: :numeric, default: nil, desc: "Port to report prometheus metrics on"
option :debug, type: :boolean, default: false, desc: "Whether to run the proxy in debug mode"
option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
def boot_config(subcommand)
say "The proxy boot_config command is deprecated - set the config in the deploy YAML at proxy/run instead", :yellow
proxy_boot_config = KAMAL.config.proxy_boot
case subcommand
when "set"
boot_options = [
*(proxy_boot_config.publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]),
*(proxy_boot_config.logging_args(options[:log_max_size])),
*("--expose=#{options[:metrics_port]}" if options[:metrics_port]),
*options[:docker_options].map { |option| "--#{option}" }
]
image = [
options[:registry].presence,
options[:repository].presence || proxy_boot_config.repository_name,
proxy_boot_config.image_name
].compact.join("/")
image_version = options[:image_version]
run_command_options = { debug: options[:debug] || nil, "metrics-port": options[:metrics_port] }.compact
run_command = "kamal-proxy run #{Kamal::Utils.optionize(run_command_options).join(" ")}" if run_command_options.any?
on(KAMAL.proxy_hosts) do |host|
proxy = KAMAL.proxy(host)
execute(*proxy.ensure_proxy_directory)
if boot_options != proxy_boot_config.default_boot_options
upload! StringIO.new(boot_options.join(" ")), proxy_boot_config.options_file
else
execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
end
if image != proxy_boot_config.image_default
upload! StringIO.new(image), proxy_boot_config.image_file
else
execute *proxy.reset_image, raise_on_non_zero_exit: false
end
if image_version
upload! StringIO.new(image_version), proxy_boot_config.image_version_file
else
execute *proxy.reset_image_version, raise_on_non_zero_exit: false
end
if run_command
upload! StringIO.new(run_command), proxy_boot_config.run_command_file
else
execute *proxy.reset_run_command, raise_on_non_zero_exit: false
end
end
when "get"
on(KAMAL.proxy_hosts) do |host|
puts "Host #{host}: #{capture_with_info(*KAMAL.proxy(host).boot_config)}"
end
when "reset"
on(KAMAL.proxy_hosts) do |host|
proxy = KAMAL.proxy(host)
execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
execute *proxy.reset_image, raise_on_non_zero_exit: false
execute *proxy.reset_image_version, raise_on_non_zero_exit: false
execute *proxy.reset_run_command, raise_on_non_zero_exit: false
end
else
raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
end
end
desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def reboot
confirming "This will cause a brief outage on each host. Are you sure?" do
with_lock do
host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ]
host_groups.each do |hosts|
host_list = Array(hosts).join(",")
run_hook "pre-proxy-reboot", hosts: host_list
on(hosts) do |host|
proxy = KAMAL.proxy(host)
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
execute *KAMAL.registry.login
"Stopping and removing kamal-proxy on #{host}, if running..."
execute *proxy.stop, raise_on_non_zero_exit: false
execute *proxy.remove_container
execute *proxy.ensure_apps_config_directory
execute *proxy.run
end
run_hook "post-proxy-reboot", hosts: host_list
end
end
end
end
desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)", hide: true
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def upgrade
invoke_options = { "version" => KAMAL.config.latest_tag }.merge(options)
confirming "This will cause a brief outage on each host. Are you sure?" do
host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ]
host_groups.each do |hosts|
host_list = Array(hosts).join(",")
say "Upgrading proxy on #{host_list}...", :magenta
run_hook "pre-proxy-reboot", hosts: host_list
on(hosts) do |host|
proxy = KAMAL.proxy(host)
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
execute *KAMAL.registry.login
"Stopping and removing Traefik on #{host}, if running..."
execute *proxy.cleanup_traefik
"Stopping and removing kamal-proxy on #{host}, if running..."
execute *proxy.stop, raise_on_non_zero_exit: false
execute *proxy.remove_container
execute *proxy.remove_image
end
KAMAL.with_specific_hosts(hosts) do
invoke "kamal:cli:proxy:boot", [], invoke_options
reset_invocation(Kamal::Cli::Proxy)
invoke "kamal:cli:app:boot", [], invoke_options
reset_invocation(Kamal::Cli::App)
invoke "kamal:cli:prune:all", [], invoke_options
reset_invocation(Kamal::Cli::Prune)
end
run_hook "post-proxy-reboot", hosts: host_list
say "Upgraded proxy on #{host_list}", :magenta
end
end
end
desc "start", "Start existing proxy container on servers"
def start
with_lock do
on(KAMAL.proxy_hosts) do |host|
execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
execute *KAMAL.proxy(host).start
end
end
end
desc "stop", "Stop existing proxy container on servers"
def stop
with_lock do
on(KAMAL.proxy_hosts) do |host|
execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
execute *KAMAL.proxy(host).stop, raise_on_non_zero_exit: false
end
end
end
desc "restart", "Restart existing proxy container on servers"
def restart
with_lock do
stop
start
end
end
desc "details", "Show details about proxy container from servers"
def details
quiet = options[:quiet]
on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy(host).info), type: "Proxy", quiet: quiet }
end
desc "logs", "Show log lines from proxy on servers"
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs
grep = options[:grep]
timestamps = !options[:skip_timestamps]
if options[:follow]
run_locally do
proxy = KAMAL.proxy(KAMAL.primary_host)
info "Following logs on #{KAMAL.primary_host}..."
info proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
exec proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
end
else
since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(KAMAL.proxy_hosts) do |host|
puts_by_host host, capture(*KAMAL.proxy(host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
end
end
end
desc "remove", "Remove proxy container and image from servers"
option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
def remove
with_lock do
if removal_allowed?(options[:force])
stop
remove_container
remove_image
remove_proxy_directory
end
end
end
desc "remove_container", "Remove proxy container from servers", hide: true
def remove_container
with_lock do
on(KAMAL.proxy_hosts) do
execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
execute *KAMAL.proxy(host).remove_container
end
end
end
desc "remove_image", "Remove proxy image from servers", hide: true
def remove_image
with_lock do
on(KAMAL.proxy_hosts) do
execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
execute *KAMAL.proxy(host).remove_image
end
end
end
desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
def remove_proxy_directory
with_lock do
on(KAMAL.proxy_hosts) do
execute *KAMAL.proxy(host).remove_proxy_directory, raise_on_non_zero_exit: false
end
end
end
private
def removal_allowed?(force)
on(KAMAL.proxy_hosts) do |host|
app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i
raise "The are other applications installed on #{host}" if app_count > 0
end
true
rescue SSHKit::Runner::ExecuteError => e
raise unless e.message.include?("The are other applications installed on")
if force
say "Forcing, so removing the proxy, even though other apps are installed", :magenta
else
say "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", :magenta
end
force
end
end
================================================
FILE: lib/kamal/cli/prune.rb
================================================
class Kamal::Cli::Prune < Kamal::Cli::Base
desc "all", "Prune unused images and stopped containers"
def all
with_lock do
containers
images
end
end
desc "images", "Prune unused images"
def images
with_lock do
on(KAMAL.hosts) do
execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
execute *KAMAL.prune.dangling_images
execute *KAMAL.prune.tagged_images
end
end
end
desc "containers", "Prune all stopped containers, except the last n (default 5)"
option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
def containers
retain = options.fetch(:retain, KAMAL.config.retain_containers)
raise "retain must be at least 1" if retain < 1
with_lock do
on(KAMAL.hosts) do
execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
execute *KAMAL.prune.app_containers(retain: retain)
end
end
end
end
================================================
FILE: lib/kamal/cli/registry.rb
================================================
class Kamal::Cli::Registry < Kamal::Cli::Base
desc "setup", "Setup local registry or log in to remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def setup
ensure_docker_installed unless options[:skip_local]
if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.setup } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
end
end
desc "remove", "Remove local registry or log out of remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def remove
if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
end
end
desc "login", "Log in to remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def login
if KAMAL.registry.local?
raise "Cannot use login command with a local registry. Use `kamal registry setup` instead."
end
setup
end
desc "logout", "Log out of remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def logout
if KAMAL.registry.local?
raise "Cannot use logout command with a local registry. Use `kamal registry remove` instead."
end
remove
end
end
================================================
FILE: lib/kamal/cli/secrets.rb
================================================
class Kamal::Cli::Secrets < Kamal::Cli::Base
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
option :account, type: :string, required: false, desc: "The account identifier or username"
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
option :inline, type: :boolean, required: false, hidden: true
def fetch(*secrets)
adapter = initialize_adapter(options[:adapter])
if adapter.requires_account? && options[:account].blank?
return puts "No value provided for required options '--account'"
end
results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
json = JSON.dump(results)
return_or_puts options[:inline] ? json.shellescape : json, inline: options[:inline]
end
desc "extract", "Extract a single secret from the results of a fetch call"
option :inline, type: :boolean, required: false, hidden: true
def extract(name, secrets)
parsed_secrets = JSON.parse(secrets)
value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
raise "Could not find secret #{name}" if value.nil?
return_or_puts value, inline: options[:inline]
end
desc "print", "Print the secrets (for debugging)"
def print
KAMAL.config.secrets.to_h.each do |key, value|
puts "#{key}=#{value}"
end
end
private
def initialize_adapter(adapter)
Kamal::Secrets::Adapters.lookup(adapter)
end
def return_or_puts(value, inline: nil)
if inline
value
else
puts value
end
end
end
================================================
FILE: lib/kamal/cli/server.rb
================================================
class Kamal::Cli::Server < Kamal::Cli::Base
desc "exec", "Run a custom command on the server (use --help to show options)"
option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
def exec(*cmd)
pre_connect_if_required
cmd = Kamal::Utils.join_commands(cmd)
hosts = KAMAL.hosts
quiet = options[:quiet]
case
when options[:interactive]
host = KAMAL.primary_host
say "Running '#{cmd}' on #{host} interactively...", :magenta
run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
else
say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
on(hosts) do |host|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
puts_by_host host, capture_with_info(cmd), quiet: quiet
end
end
end
desc "bootstrap", "Set up Docker to run Kamal apps"
def bootstrap
with_lock do
missing = []
on(KAMAL.hosts) do |host|
unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
info "Missing Docker on #{host}. Installing…"
execute *KAMAL.docker.install
unless execute(*KAMAL.docker.root?, raise_on_non_zero_exit: false) ||
execute(*KAMAL.docker.in_docker_group?, raise_on_non_zero_exit: false)
execute *KAMAL.docker.add_to_docker_group
begin
execute *KAMAL.docker.refresh_session
rescue IOError
info "Session refreshed due to group change."
end
end
else
missing << host
end
end
end
if missing.any?
raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
end
run_hook "docker-setup"
end
end
end
================================================
FILE: lib/kamal/cli/templates/deploy.yml
================================================
# Name of your application. Used to uniquely configure containers.
service: my-app
# Name of the container image.
image: my-user/my-app
# Deploy to these servers.
servers:
web:
- 192.168.0.1
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
ssl: true
host: app.example.com
# Proxy connects to your container on port 80 by default.
# app_port: 3000
# Credentials for your image host.
registry:
server: localhost:5555
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
# username: my-user
# Always use an access token rather than real password (pulled from .kamal/secrets).
# password:
# - KAMAL_REGISTRY_PASSWORD
# Configure builder setup.
builder:
arch: amd64
# Pass in additional build args needed for your Dockerfile.
# args:
# RUBY_VERSION: <%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %>
# Inject ENV variables into containers (secrets come from .kamal/secrets).
#
# env:
# clear:
# DB_HOST: 192.168.0.2
# secret:
# - RAILS_MASTER_KEY
# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal app logs -r job" will tail logs from the first server in the job section.
#
# aliases:
# shell: app exec --interactive --reuse "bash"
# Use a different ssh user than root
#
# ssh:
# user: app
# Use a persistent storage volume.
#
# volumes:
# - "app_storage:/app/storage"
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
#
# asset_path: /app/public/assets
# Configure rolling deploys by setting a wait time between batches of restarts.
#
# boot:
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
# wait: 2
# Use accessory services (secrets come from .kamal/secrets).
#
# accessories:
# db:
# image: mysql:8.0
# host: 192.168.0.2
# port: 3306
# env:
# clear:
# MYSQL_ROOT_HOST: '%'
# secret:
# - MYSQL_ROOT_PASSWORD
# files:
# - config/mysql/production.cnf:/etc/mysql/my.cnf
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
# directories:
# - data:/var/lib/mysql
# redis:
# image: valkey/valkey:8
# host: 192.168.0.2
# port: 6379
# directories:
# - data:/data
================================================
FILE: lib/kamal/cli/templates/sample_hooks/docker-setup.sample
================================================
#!/bin/sh
echo "Docker set up on $KAMAL_HOSTS..."
================================================
FILE: lib/kamal/cli/templates/sample_hooks/post-app-boot.sample
================================================
#!/bin/sh
echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
================================================
FILE: lib/kamal/cli/templates/sample_hooks/post-deploy.sample
================================================
#!/bin/sh
# A sample post-deploy hook
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
================================================
FILE: lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample
================================================
#!/bin/sh
echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample
================================================
#!/bin/sh
echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-build.sample
================================================
#!/bin/sh
# A sample pre-build hook
#
# Checks:
# 1. We have a clean checkout
# 2. A remote is configured
# 3. The branch has been pushed to the remote
# 4. The version we are deploying matches the remote
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)
if [ -n "$(git status --porcelain)" ]; then
echo "Git checkout is not clean, aborting..." >&2
git status --porcelain >&2
exit 1
fi
first_remote=$(git remote)
if [ -z "$first_remote" ]; then
echo "No git remote set, aborting..." >&2
exit 1
fi
current_branch=$(git branch --show-current)
if [ -z "$current_branch" ]; then
echo "Not on a git branch, aborting..." >&2
exit 1
fi
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
if [ -z "$remote_head" ]; then
echo "Branch not pushed to remote, aborting..." >&2
exit 1
fi
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
exit 1
fi
exit 0
================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-connect.sample
================================================
#!/usr/bin/env ruby
# A sample pre-connect check
#
# Warms DNS before connecting to hosts in parallel
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
hosts = ENV["KAMAL_HOSTS"].split(",")
results = nil
max = 3
elapsed = Benchmark.realtime do
results = hosts.map do |host|
Thread.new do
tries = 1
begin
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
rescue SocketError
if tries < max
puts "Retrying DNS warmup: #{host}"
tries += 1
sleep rand
retry
else
puts "DNS warmup failed: #{host}"
host
end
end
tries
end
end.map(&:value)
end
retries = results.sum - hosts.size
nopes = results.count { |r| r == max }
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-deploy.sample
================================================
#!/usr/bin/env ruby
# A sample pre-deploy hook
#
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
#
# Fails unless the combined status is "success"
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_COMMAND
# KAMAL_SUBCOMMAND
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)
# Only check the build status for production deployments
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
exit 0
end
require "bundler/inline"
# true = install gems so this is fast on repeat invocations
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "octokit"
gem "faraday-retry"
end
MAX_ATTEMPTS = 72
ATTEMPTS_GAP = 10
def exit_with_error(message)
$stderr.puts message
exit 1
end
class GithubStatusChecks
attr_reader :remote_url, :git_sha, :github_client, :combined_status
def initialize
@remote_url = github_repo_from_remote_url
@git_sha = `git rev-parse HEAD`.strip
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
refresh!
end
def refresh!
@combined_status = github_client.combined_status(remote_url, git_sha)
end
def state
combined_status[:state]
end
def first_status_url
first_status = combined_status[:statuses].find { |status| status[:state] == state }
first_status && first_status[:target_url]
end
def complete_count
combined_status[:statuses].count { |status| status[:state] != "pending"}
end
def total_count
combined_status[:statuses].count
end
def current_status
if total_count > 0
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
else
"Build not started..."
end
end
private
def github_repo_from_remote_url
url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
if url.start_with?("https://github.com/")
url.delete_prefix("https://github.com/")
elsif url.start_with?("git@github.com:")
url.delete_prefix("git@github.com:")
else
url
end
end
end
$stdout.sync = true
begin
puts "Checking build status..."
attempts = 0
checks = GithubStatusChecks.new
loop do
case checks.state
when "success"
puts "Checks passed, see #{checks.first_status_url}"
exit 0
when "failure"
exit_with_error "Checks failed, see #{checks.first_status_url}"
when "pending"
attempts += 1
end
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
puts checks.current_status
sleep(ATTEMPTS_GAP)
checks.refresh!
end
rescue Octokit::NotFound
exit_with_error "Build status could not be found"
end
================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample
================================================
#!/bin/sh
echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
================================================
FILE: lib/kamal/cli/templates/secrets
================================================
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
# Option 1: Read secrets from the environment
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
# Option 2: Read secrets via a command
# RAILS_MASTER_KEY=$(cat config/master.key)
# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
# Option 3: Read secrets via kamal secrets helpers
# These will handle logging in and fetching the secrets in as few calls as possible
# There are adapters for 1Password, LastPass + Bitwarden
#
# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
================================================
FILE: lib/kamal/cli.rb
================================================
module Kamal::Cli
class BootError < StandardError; end
class HookError < StandardError; end
class LockError < StandardError; end
class DependencyError < StandardError; end
end
# SSHKit uses instance eval, so we need a global const for ergonomics
KAMAL = Kamal::Commander.new
================================================
FILE: lib/kamal/commander/specifics.rb
================================================
class Kamal::Commander::Specifics
attr_reader :primary_host, :primary_role, :hosts, :roles
delegate :stable_sort!, to: Kamal::Utils
def initialize(config, specific_hosts, specific_roles)
@config, @specific_hosts, @specific_roles = config, specific_hosts, specific_roles
@roles, @hosts = specified_roles, specified_hosts
@primary_host = specific_hosts&.first || primary_specific_role&.primary_host || config.primary_host
@primary_role = primary_or_first_role(roles_on(primary_host))
stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
sort_primary_role_hosts_first!(hosts)
end
def roles_on(host)
roles.select { |role| role.hosts.include?(host.to_s) }
end
def app_hosts
@app_hosts ||= sort_primary_role_hosts_first!(config.app_hosts & specified_hosts)
end
def proxy_hosts
config.proxy_hosts & specified_hosts
end
def accessory_hosts
config.accessories.flat_map(&:hosts) & specified_hosts
end
private
attr_reader :config, :specific_hosts, :specific_roles
def primary_specific_role
primary_or_first_role(specific_roles) if specific_roles.present?
end
def primary_or_first_role(roles)
roles.detect { |role| role == config.primary_role } || roles.first
end
def specified_roles
(specific_roles || config.roles) \
.select { |role| ((specific_hosts || config.all_hosts) & role.hosts).any? }
end
def specified_hosts
specified_hosts = specific_hosts || config.all_hosts
if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
specified_hosts.select { |host| specific_role_hosts.include?(host) }
else
specified_hosts
end
end
def sort_primary_role_hosts_first!(hosts)
stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
end
end
================================================
FILE: lib/kamal/commander.rb
================================================
require "active_support/core_ext/enumerable"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/object/blank"
class Kamal::Commander
attr_accessor :verbosity, :holding_lock, :connected
attr_reader :specific_roles, :specific_hosts
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
def initialize
reset
end
def reset
self.verbosity = :info
self.holding_lock = ENV["KAMAL_LOCK"] == "true"
self.connected = false
@specifics = @specific_roles = @specific_hosts = nil
@config = @config_kwargs = nil
@commands = {}
end
def config
@config ||= Kamal::Configuration.create_from(**@config_kwargs.to_h).tap do |config|
@config_kwargs = nil
configure_sshkit_with(config)
end
end
def configure(**kwargs)
@config, @config_kwargs = nil, kwargs
end
def configured?
@config || @config_kwargs
end
def specific_primary!
@specifics = nil
if specific_roles.present?
self.specific_hosts = [ specific_roles.first.primary_host ]
else
self.specific_hosts = [ config.primary_host ]
end
end
def specific_roles=(role_names)
@specifics = nil
@specific_roles = if role_names.present?
filtered = Kamal::Utils.filter_specific_items(role_names, config.roles)
raise ArgumentError, "No --roles match for #{role_names.join(',')}" if filtered.empty?
filtered
end
end
def specific_hosts=(hosts)
@specifics = nil
@specific_hosts = if hosts.present?
filtered = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
raise ArgumentError, "No --hosts match for #{hosts.join(',')}" if filtered.empty?
filtered
end
end
def with_specific_hosts(hosts)
original_hosts, self.specific_hosts = specific_hosts, hosts
yield
ensure
self.specific_hosts = original_hosts
end
def accessory_names
config.accessories&.collect(&:name) || []
end
def app(role: nil, host: nil)
Kamal::Commands::App.new(config, role: role, host: host)
end
def accessory(name)
Kamal::Commands::Accessory.new(config, name: name)
end
def auditor(**details)
Kamal::Commands::Auditor.new(config, **details)
end
def builder
@commands[:builder] ||= Kamal::Commands::Builder.new(config)
end
def docker
@commands[:docker] ||= Kamal::Commands::Docker.new(config)
end
def hook
@commands[:hook] ||= Kamal::Commands::Hook.new(config)
end
def lock
@commands[:lock] ||= Kamal::Commands::Lock.new(config)
end
def proxy(host)
Kamal::Commands::Proxy.new(config, host: host)
end
def prune
@commands[:prune] ||= Kamal::Commands::Prune.new(config)
end
def registry
@commands[:registry] ||= Kamal::Commands::Registry.new(config)
end
def server
@commands[:server] ||= Kamal::Commands::Server.new(config)
end
def alias(name)
config.aliases[name]
end
def resolve_alias(name)
if @config
@config.aliases[name]&.command
else
raw_config = Kamal::Configuration.load_raw_config(**@config_kwargs.to_h.slice(:config_file, :destination))
raw_config[:aliases]&.dig(name)
end
end
def with_verbosity(level)
old_level = self.verbosity
self.verbosity = level
SSHKit.config.output_verbosity = level
yield
ensure
self.verbosity = old_level
SSHKit.config.output_verbosity = old_level
end
def holding_lock?
self.holding_lock
end
def connected?
self.connected
end
private
# Lazy setup of SSHKit
def configure_sshkit_with(config)
SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
SSHKit::Backend::Netssh.configure do |sshkit|
sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
sshkit.dns_retries = config.sshkit.dns_retries
sshkit.ssh_options = config.ssh.options
end
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
SSHKit.config.output_verbosity = verbosity
end
def specifics
@specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
end
end
================================================
FILE: lib/kamal/commands/accessory/proxy.rb
================================================
module Kamal::Commands::Accessory::Proxy
delegate :container_name, to: :"config.proxy_boot", prefix: :proxy
def deploy(target:)
proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
end
def remove
proxy_exec :remove, service_name
end
private
def proxy_exec(*command)
docker :exec, proxy_container_name, "kamal-proxy", *command
end
end
================================================
FILE: lib/kamal/commands/accessory.rb
================================================
class Kamal::Commands::Accessory < Kamal::Commands::Base
include Proxy
attr_reader :accessory_config
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
:secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
to: :accessory_config
def initialize(config, name:)
super(config)
@accessory_config = config.accessory(name)
end
def run(host: nil)
docker :run,
"--name", service_name,
"--detach",
"--restart", "unless-stopped",
*network_args,
*config.logging_args,
*publish_args,
*([ "--env", "KAMAL_HOST=\"#{host}\"" ] if host),
*env_args,
*volume_args,
*label_args,
*option_args,
image,
cmd
end
def start
docker :container, :start, service_name
end
def stop
docker :container, :stop, service_name
end
def info(all: false, quiet: false)
docker :ps, *("-a" if all), *("-q" if quiet), *service_filter
end
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end
def follow_logs(timestamps: true, grep: nil, grep_options: nil)
run_over_ssh \
pipe \
docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
end
def execute_in_existing_container(*command, interactive: false)
docker :exec,
(docker_interactive_args if interactive),
service_name,
*command
end
def execute_in_new_container(*command, interactive: false)
docker :run,
(docker_interactive_args if interactive),
"--rm",
*network_args,
*env_args,
*volume_args,
*option_args,
image,
*command
end
def execute_in_existing_container_over_ssh(*command)
run_over_ssh execute_in_existing_container(*command, interactive: true)
end
def execute_in_new_container_over_ssh(*command)
run_over_ssh execute_in_new_container(*command, interactive: true)
end
def run_over_ssh(command)
super command, host: hosts.first
end
def ensure_local_file_present(local_file)
if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
raise "Missing file: #{local_file}"
end
end
def pull_image
docker :image, :pull, image
end
def remove_service_directory
[ :rm, "-rf", service_name ]
end
def remove_container
docker :container, :prune, "--force", *service_filter
end
def remove_image
docker :image, :rm, "--force", image
end
def ensure_env_directory
make_directory env_directory
end
private
def service_filter
[ "--filter", "label=service=#{service_name}" ]
end
end
================================================
FILE: lib/kamal/commands/app/assets.rb
================================================
module Kamal::Commands::App::Assets
def extract_assets
asset_container = "#{role.container_prefix}-assets"
combine \
make_directory(role.asset_extracted_directory),
[ *docker(:container, :rm, asset_container, "2> /dev/null"), "|| true" ],
docker(:container, :create, "--name", asset_container, config.absolute_image),
docker(:container, :cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory),
docker(:container, :rm, asset_container),
by: "&&"
end
def sync_asset_volumes(old_version: nil)
new_extracted_path, new_volume_path = role.asset_extracted_directory(config.version), role.asset_volume.host_path
if old_version.present?
old_extracted_path, old_volume_path = role.asset_extracted_directory(old_version), role.asset_volume(old_version).host_path
end
commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ]
if old_version.present?
commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
commands << copy_contents(old_extracted_path, new_volume_path, continue_on_error: true)
end
chain *commands
end
def clean_up_assets
chain \
find_and_remove_older_siblings(role.asset_extracted_directory),
find_and_remove_older_siblings(role.asset_volume_directory)
end
private
def find_and_remove_older_siblings(path)
[
:find,
Pathname.new(path).dirname.to_s,
"-maxdepth 1",
"-name", "'#{role.name}-*'",
"!", "-name", Pathname.new(path).basename.to_s,
"-exec rm -rf \"{}\" +"
]
end
def copy_contents(source, destination, continue_on_error: false)
[ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error) ]
end
end
================================================
FILE: lib/kamal/commands/app/containers.rb
================================================
module Kamal::Commands::App::Containers
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
def list_containers
docker :container, :ls, "--all", *container_filter_args
end
def list_container_names
[ *list_containers, "--format", "'{{ .Names }}'" ]
end
def remove_container(version:)
pipe \
container_id_for(container_name: container_name(version)),
xargs(docker(:container, :rm))
end
def rename_container(version:, new_version:)
docker :rename, container_name(version), container_name(new_version)
end
def remove_containers
docker :container, :prune, "--force", *container_filter_args
end
def container_health_log(version:)
pipe \
container_id_for(container_name: container_name(version)),
xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
end
end
================================================
FILE: lib/kamal/commands/app/error_pages.rb
================================================
module Kamal::Commands::App::ErrorPages
def create_error_pages_directory
make_directory(config.proxy_boot.error_pages_directory)
end
def clean_up_error_pages
[ :find, config.proxy_boot.error_pages_directory, "-mindepth", "1", "-maxdepth", "1", "!", "-name", KAMAL.config.version, "-exec", "rm", "-rf", "{} +" ]
end
end
================================================
FILE: lib/kamal/commands/app/execution.rb
================================================
module Kamal::Commands::App::Execution
def execute_in_existing_container(*command, interactive: false, env:)
docker :exec,
(docker_interactive_args if interactive),
*argumentize("--env", env),
container_name,
*command
end
def execute_in_new_container(*command, interactive: false, detach: false, env:)
docker :run,
(docker_interactive_args if interactive),
("--detach" if detach),
("--rm" unless detach),
"--name", container_name_for_exec,
"--network", "kamal",
*role&.env_args(host),
*argumentize("--env", env),
*role.logging_args,
*config.volume_args,
*role&.option_args,
config.absolute_image,
*command
end
def execute_in_existing_container_over_ssh(*command, env:)
run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
end
def execute_in_new_container_over_ssh(*command, env:)
run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
end
private
def container_name_for_exec
[ role.container_prefix, "exec", config.version, SecureRandom.hex(3) ].compact.join("-")
end
end
================================================
FILE: lib/kamal/commands/app/images.rb
================================================
module Kamal::Commands::App::Images
def list_images
docker :image, :ls, config.repository
end
def remove_images
docker :image, :prune, "--all", "--force", *image_filter_args
end
def tag_latest_image
docker :tag, config.absolute_image, config.latest_image
end
end
================================================
FILE: lib/kamal/commands/app/logging.rb
================================================
module Kamal::Commands::App::Logging
def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
container_id_command(container_id),
"xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end
def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil)
run_over_ssh \
pipe(
container_id_command(container_id),
"xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
),
host: host
end
private
def container_id_command(container_id)
case container_id
when Array then container_id
when String, Symbol then "echo #{container_id}"
else current_running_container_id
end
end
end
================================================
FILE: lib/kamal/commands/app/proxy.rb
================================================
module Kamal::Commands::App::Proxy
delegate :container_name, to: :"config.proxy_boot", prefix: :proxy
def deploy(target:)
proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
end
def remove
proxy_exec :remove, role.container_prefix
end
def live
proxy_exec :resume, role.container_prefix
end
def maintenance(**options)
proxy_exec :stop, role.container_prefix, *role.proxy.stop_command_args(**options)
end
def remove_proxy_app_directory
remove_directory config.proxy_boot.app_directory
end
def create_ssl_directory
make_directory(File.join(config.proxy_boot.tls_directory, role.name))
end
private
def proxy_exec(*command)
docker :exec, proxy_container_name, "kamal-proxy", *command
end
end
================================================
FILE: lib/kamal/commands/app.rb
================================================
class Kamal::Commands::App < Kamal::Commands::Base
include Assets, Containers, ErrorPages, Execution, Images, Logging, Proxy
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
attr_reader :role, :host
delegate :container_name, to: :role
def initialize(config, role: nil, host: nil)
super(config)
@role = role
@host = host
end
def run(hostname: nil)
docker :run,
"--detach",
"--restart unless-stopped",
"--name", container_name,
"--network", "kamal",
*([ "--hostname", hostname ] if hostname),
"--env", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
"--env", "KAMAL_VERSION=\"#{config.version}\"",
"--env", "KAMAL_HOST=\"#{host}\"",
*([ "--env", "KAMAL_DESTINATION=\"#{config.destination}\"" ] if config.destination),
*role.env_args(host),
*role.logging_args,
*config.volume_args,
*role.asset_volume_args,
*role.label_args,
*role.option_args,
config.absolute_image,
role.cmd
end
def start
docker :start, container_name
end
def status(version:)
pipe container_id_for_version(version), xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
end
def stop(version: nil)
pipe \
version ? container_id_for_version(version) : current_running_container_id,
xargs(docker(:stop, *role.stop_args))
end
def info
docker :ps, *container_filter_args
end
def current_running_container_id
current_running_container(format: "--quiet")
end
def container_id_for_version(version, only_running: false)
container_id_for(container_name: container_name(version), only_running: only_running)
end
def current_running_version
pipe \
current_running_container(format: "--format '{{.Names}}'"),
extract_version_from_name
end
def list_versions(*docker_args, statuses: nil)
pipe \
docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
extract_version_from_name
end
def ensure_env_directory
make_directory role.env_directory
end
private
def latest_image_id
docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
end
def current_running_container(format:)
pipe \
shell(chain(latest_image_container(format: format), latest_container(format: format))),
[ :head, "-1" ]
end
def latest_image_container(format:)
latest_container format: format, filters: [ "ancestor=$(#{latest_image_id.join(" ")})" ]
end
def latest_container(format:, filters: nil)
docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
end
def container_filter_args(statuses: nil)
argumentize "--filter", container_filters(statuses: statuses)
end
def image_filter_args
argumentize "--filter", image_filters
end
def extract_version_from_name
# Extract SHA from "service-role-dest-SHA"
%(while read line; do echo ${line##{role.container_prefix}-}; done)
end
def container_filters(statuses: nil)
[ "label=service=#{config.service}" ].tap do |filters|
filters << "label=destination=#{config.destination}"
filters << "label=role=#{role}" if role
statuses&.each do |status|
filters << "status=#{status}"
end
end
end
def image_filters
[ "label=service=#{config.service}" ]
end
end
================================================
FILE: lib/kamal/commands/auditor.rb
================================================
class Kamal::Commands::Auditor < Kamal::Commands::Base
attr_reader :details
delegate :escape_shell_value, to: Kamal::Utils
def initialize(config, **details)
super(config)
@details = details
end
# Runs remotely
def record(line, **details)
combine \
make_run_directory,
append([ :echo, escape_shell_value(audit_line(line, **details)) ], audit_log_file)
end
def reveal
[ :tail, "-n", 50, audit_log_file ]
end
private
def audit_log_file
file = [ config.service, config.destination, "audit.log" ].compact.join("-")
File.join(config.run_directory, file)
end
def audit_tags(**details)
tags(**self.details, **details)
end
def make_run_directory
[ :mkdir, "-p", config.run_directory ]
end
def audit_line(line, **details)
"#{audit_tags(**details).except(:version, :service_version, :service)} #{line}"
end
end
================================================
FILE: lib/kamal/commands/base.rb
================================================
module Kamal::Commands
class Base
delegate :sensitive, :argumentize, to: Kamal::Utils
DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
attr_accessor :config
def initialize(config)
@config = config
end
def run_over_ssh(*command, host:)
"ssh#{ssh_config_args}#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
end
def container_id_for(container_name:, only_running: false)
docker :container, :ls, *("--all" unless only_running), "--filter", "'name=^#{container_name}$'", "--quiet"
end
def make_directory_for(remote_file)
make_directory Pathname.new(remote_file).dirname.to_s
end
def make_directory(path)
[ :mkdir, "-p", path ]
end
def remove_directory(path)
[ :rm, "-r", path ]
end
def remove_file(path)
[ :rm, path ]
end
def ensure_docker_installed
combine \
ensure_local_docker_installed,
ensure_local_buildx_installed
end
private
def combine(*commands, by: "&&")
commands
.compact
.collect { |command| Array(command) + [ by ] }.flatten # Join commands
.tap { |commands| commands.pop } # Remove trailing combiner
end
def chain(*commands)
combine *commands, by: ";"
end
def pipe(*commands)
combine *commands, by: "|"
end
def append(*commands)
combine *commands, by: ">>"
end
def write(*commands)
combine *commands, by: ">"
end
def any(*commands)
combine *commands, by: "||"
end
def substitute(*commands)
"\$\(#{commands.join(" ")}\)"
end
def xargs(command)
[ :xargs, command ].flatten
end
def shell(command)
[ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ]
end
def docker(*args)
args.compact.unshift :docker
end
def pack(*args)
args.compact.unshift :pack
end
def git(*args, path: nil)
[ :git, *([ "-C", path ] if path), *args.compact ]
end
def grep(*args)
args.compact.unshift :grep
end
def tags(**details)
Kamal::Tags.from_config(config, **details)
end
def ssh_config_args
case config.ssh.config
when Array
config.ssh.config.map { |file| " -F #{file}" }.join
when String
" -F #{config.ssh.config}"
when true
"" # Use default SSH config
when false
" -F /dev/null" # Ignore SSH config
end
end
def ssh_proxy_args
case config.ssh.proxy
when Net::SSH::Proxy::Jump
" -J #{config.ssh.proxy.jump_proxies}"
when Net::SSH::Proxy::Command
" -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
end
end
def ssh_keys_args
"#{ ssh_keys.join("") if ssh_keys}" + "#{" -o IdentitiesOnly=yes" if config.ssh&.keys_only}"
end
def ssh_keys
config.ssh.keys&.map do |key|
" -i #{key}"
end
end
def ensure_local_docker_installed
docker "--version"
end
def ensure_local_buildx_installed
docker :buildx, "version"
end
def docker_interactive_args
STDIN.isatty ? "-it" : "-i"
end
end
end
================================================
FILE: lib/kamal/commands/builder/base.rb
================================================
class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError < StandardError; end
ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'"
delegate :argumentize, to: Kamal::Utils
delegate \
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
:pack?, :pack_builder, :pack_buildpacks,
:cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
to: :builder_config
def clean
docker :image, :rm, "--force", config.absolute_image
end
def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
docker :buildx, :build,
"--output=type=#{export_action}",
*platform_options(arches),
*([ "--builder", builder_name ] unless docker_driver?),
*build_tag_options(tag_as_dirty: tag_as_dirty),
*build_options,
*([ "--no-cache" ] if no_cache),
build_context,
"2>&1"
end
def pull
docker :pull, config.absolute_image
end
def info
combine \
docker(:context, :ls),
docker(:buildx, :ls)
end
def inspect_builder
docker :buildx, :inspect, builder_name unless docker_driver?
end
def build_options
[ *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
end
def build_context
config.builder.context
end
def validate_image
pipe \
docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
any(
[ :grep, "-x", config.service ],
"(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
)
end
def first_mirror
docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
end
def login_to_registry_locally?
true
end
def push_env
{}
end
private
def build_tag_names(tag_as_dirty: false)
tag_names = [ config.absolute_image, config.latest_image ]
tag_names.map! { |t| "#{t}-dirty" } if tag_as_dirty
tag_names
end
def build_tag_options(tag_as_dirty: false)
build_tag_names(tag_as_dirty: tag_as_dirty).flat_map { |name| [ "-t", name ] }
end
def build_cache
if cache_to && cache_from
[ "--cache-to", cache_to,
"--cache-from", cache_from ]
end
end
def build_labels
argumentize "--label", { service: config.service }
end
def build_args
argumentize "--build-arg", args, sensitive: true
end
def build_secrets
argumentize "--secret", secrets.keys.collect { |secret| [ "id", secret ] }
end
def build_dockerfile
if Pathname.new(File.expand_path(dockerfile)).exist?
argumentize "--file", dockerfile
else
raise BuilderError, "Missing #{dockerfile}"
end
end
def build_target
argumentize "--target", target if target.present?
end
def build_ssh
argumentize "--ssh", ssh if ssh.present?
end
def builder_provenance
argumentize "--provenance", provenance unless provenance.nil?
end
def builder_sbom
argumentize "--sbom", sbom unless sbom.nil?
end
def builder_config
config.builder
end
def registry_config
config.registry
end
def driver_options
if registry_config.local?
[ "--driver-opt", "network=host" ]
end
end
def platform_options(arches)
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
end
end
================================================
FILE: lib/kamal/commands/builder/clone.rb
================================================
module Kamal::Commands::Builder::Clone
def clone
git :clone, escaped_root, "--recurse-submodules", path: config.builder.clone_directory.shellescape
end
def clone_reset_steps
[
git(:remote, "set-url", :origin, escaped_root, path: escaped_build_directory),
git(:fetch, :origin, path: escaped_build_directory),
git(:reset, "--hard", Kamal::Git.revision, path: escaped_build_directory),
git(:clean, "-fdx", path: escaped_build_directory),
git(:submodule, :update, "--init", path: escaped_build_directory)
]
end
def clone_status
git :status, "--porcelain", path: escaped_build_directory
end
def clone_revision
git :"rev-parse", :HEAD, path: escaped_build_directory
end
def escaped_root
Kamal::Git.root.shellescape
end
def escaped_build_directory
config.builder.build_directory.shellescape
end
end
================================================
FILE: lib/kamal/commands/builder/cloud.rb
================================================
class Kamal::Commands::Builder::Cloud < Kamal::Commands::Builder::Base
# Expects `driver` to be of format "cloud docker-org-name/builder-name"
def create
docker :buildx, :create, "--driver", driver
end
def remove
docker :buildx, :rm, builder_name
end
private
def builder_name
driver.gsub(/[ \/]/, "-")
end
def inspect_buildx
pipe \
docker(:buildx, :inspect, builder_name),
grep("-q", "Endpoint:.*cloud://.*")
end
end
================================================
FILE: lib/kamal/commands/builder/hybrid.rb
================================================
class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
def create
combine \
create_local_buildx,
create_remote_context,
append_remote_buildx
end
private
def builder_name
"kamal-hybrid-#{driver}-#{remote_builder_name_suffix}"
end
def create_local_buildx
docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}", *driver_options
end
def append_remote_buildx
docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, *driver_options, remote_context_name
end
end
================================================
FILE: lib/kamal/commands/builder/local.rb
================================================
class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
def create
return if docker_driver?
docker :buildx, :create, "--name", builder_name, "--driver=#{driver}", *driver_options
end
def remove
docker :buildx, :rm, builder_name unless docker_driver?
end
private
def builder_name
if registry_config.local?
"kamal-local-registry-#{driver}"
else
"kamal-local-#{driver}"
end
end
end
================================================
FILE: lib/kamal/commands/builder/pack.rb
================================================
class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
combine \
build(tag_as_dirty: tag_as_dirty, no_cache: no_cache),
export(export_action)
end
def remove;end
def info
pack :builder, :inspect, pack_builder
end
alias_method :inspect_builder, :info
private
def build(tag_as_dirty: false, no_cache: false)
pack(:build,
config.repository,
"--platform", platform,
"--creation-time", "now",
"--builder", pack_builder,
buildpacks,
*build_tag_options(tag_as_dirty: tag_as_dirty),
*([ "--clear-cache" ] if no_cache),
"--env", "BP_IMAGE_LABELS=service=#{config.service}",
*argumentize("--env", args),
*argumentize("--env", secrets, sensitive: true),
"--path", build_context)
end
def export(export_action)
return unless export_action == "registry"
combine \
docker(:push, config.absolute_image),
docker(:push, config.latest_image)
end
def platform
"linux/#{local_arches.first}"
end
def buildpacks
(pack_buildpacks << "paketo-buildpacks/image-labels").map { |buildpack| [ "--buildpack", buildpack ] }
end
end
================================================
FILE: lib/kamal/commands/builder/remote.rb
================================================
class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
def create
chain \
create_remote_context,
create_buildx
end
def remove
chain \
remove_remote_context,
remove_buildx
end
def info
chain \
docker(:context, :ls),
docker(:buildx, :ls)
end
def inspect_builder
combine \
combine(inspect_buildx, inspect_remote_context),
[ "(echo no compatible builder && exit 1)" ],
by: "||"
end
def login_to_registry_locally?
false
end
def push_env
{ "BUILDKIT_NO_CLIENT_TOKEN" => "1" }
end
private
def builder_name
"kamal-remote-#{remote_builder_name_suffix}"
end
def remote_context_name
"#{builder_name}-context"
end
def remote_builder_name_suffix
"#{remote.gsub(/[^a-z0-9_-]/, "-")}#{registry_config.local? ? "-local-registry" : "" }"
end
def inspect_buildx
pipe \
docker(:buildx, :inspect, builder_name),
grep("-q", "Endpoint:.*#{remote_context_name}")
end
def inspect_remote_context
pipe \
docker(:context, :inspect, remote_context_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT),
grep("-xq", remote)
end
def create_remote_context
docker :context, :create, remote_context_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'"
end
def remove_remote_context
docker :context, :rm, remote_context_name
end
def create_buildx
docker :buildx, :create, "--name", builder_name, *driver_options, remote_context_name
end
def remove_buildx
docker :buildx, :rm, builder_name
end
end
================================================
FILE: lib/kamal/commands/builder.rb
================================================
require "active_support/core_ext/string/filters"
class Kamal::Commands::Builder < Kamal::Commands::Base
delegate \
:create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder,
:validate_image, :first_mirror, :login_to_registry_locally?, :push_env,
to: :target
delegate \
:local?, :remote?, :pack?, :cloud?,
to: "config.builder"
include Clone
def name
target.class.to_s.remove("Kamal::Commands::Builder::").underscore.inquiry
end
def target
if remote?
if local?
hybrid
else
remote
end
elsif pack?
pack
elsif cloud?
cloud
else
local
end
end
def remote
@remote ||= Kamal::Commands::Builder::Remote.new(config)
end
def local
@local ||= Kamal::Commands::Builder::Local.new(config)
end
def hybrid
@hybrid ||= Kamal::Commands::Builder::Hybrid.new(config)
end
def pack
@pack ||= Kamal::Commands::Builder::Pack.new(config)
end
def cloud
@cloud ||= Kamal::Commands::Builder::Cloud.new(config)
end
end
================================================
FILE: lib/kamal/commands/docker.rb
================================================
class Kamal::Commands::Docker < Kamal::Commands::Base
# Install Docker using the https://github.com/docker/docker-install convenience script.
def install
pipe get_docker, :sh
end
# Checks the Docker client version. Fails if Docker is not installed.
def installed?
docker "-v"
end
# Checks the Docker server version. Fails if Docker is not running.
def running?
docker :version
end
# Do we have superuser access to install Docker and start system services?
def superuser?
[ '[ "${EUID:-$(id -u)}" -eq 0 ] || sudo -nl usermod >/dev/null' ]
end
def root?
[ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
end
def in_docker_group?
[ 'id -nG "${USER:-$(id -un)}" | grep -qw docker' ]
end
def add_to_docker_group
[ 'sudo -n usermod -aG docker "${USER:-$(id -un)}"' ]
end
def refresh_session
[ "kill -HUP $PPID" ]
end
def create_network
docker :network, :create, :kamal
end
private
def get_docker
shell \
any \
[ :curl, "-fsSL", "https://get.docker.com" ],
[ :wget, "-O -", "https://get.docker.com" ],
[ :echo, "\"exit 1\"" ]
end
end
================================================
FILE: lib/kamal/commands/hook.rb
================================================
class Kamal::Commands::Hook < Kamal::Commands::Base
def run(hook)
[ hook_file(hook) ]
end
def env(secrets: false, **details)
tags(**details).env.tap do |env|
env.merge!(config.secrets.to_h) if secrets
end
end
def hook_exists?(hook)
Pathname.new(hook_file(hook)).exist?
end
private
def hook_file(hook)
File.join(config.hooks_path, hook)
end
end
================================================
FILE: lib/kamal/commands/lock.rb
================================================
require "active_support/duration"
require "time"
require "base64"
class Kamal::Commands::Lock < Kamal::Commands::Base
def acquire(message, version)
combine \
[ :mkdir, lock_dir ],
write_lock_details(message, version)
end
def release
combine \
[ :rm, lock_details_file ],
[ :rm, "-r", lock_dir ]
end
def status
combine \
stat_lock_dir,
read_lock_details
end
def ensure_locks_directory
[ :mkdir, "-p", locks_dir ]
end
private
def write_lock_details(message, version)
write \
[ :echo, "\"#{Base64.encode64(lock_details(message, version))}\"" ],
lock_details_file
end
def read_lock_details
pipe \
[ :cat, lock_details_file ],
[ :base64, "-d" ]
end
def stat_lock_dir
write \
[ :stat, lock_dir ],
"/dev/null"
end
def lock_dir
dir_name = [ "lock", config.service, config.destination ].compact.join("-")
File.join(config.run_directory, dir_name)
end
def lock_details_file
File.join(lock_dir, "details")
end
def lock_details(message, version)
<<~DETAILS.strip
Locked by: #{locked_by} at #{Time.now.utc.iso8601}
Version: #{version}
Message: #{message}
DETAILS
end
def locked_by
Kamal::Git.user_name
rescue Errno::ENOENT
"Unknown"
end
end
================================================
FILE: lib/kamal/commands/proxy.rb
================================================
class Kamal::Commands::Proxy < Kamal::Commands::Base
delegate :argumentize, :optionize, to: Kamal::Utils
attr_reader :proxy_run_config
def initialize(config, host:)
super(config)
@proxy_run_config = config.proxy_run(host)
end
def run
if proxy_run_config
docker \
:run,
"--name", container_name,
"--network", "kamal",
"--detach",
"--restart", "unless-stopped",
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
*proxy_run_config.docker_options_args,
*proxy_run_config.image,
*proxy_run_config.run_command
else
pipe boot_config, xargs(docker_run)
end
end
def start
docker :container, :start, container_name
end
def stop(name: container_name)
docker :container, :stop, name
end
def start_or_run
combine start, run, by: "||"
end
def info
docker :ps, "--filter", "'name=^#{container_name}$'"
end
def version
pipe \
docker(:inspect, container_name, "--format '{{.Config.Image}}'"),
[ :awk, "-F:", "'{print \$NF}'" ]
end
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end
def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
run_over_ssh pipe(
docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
).join(" "), host: host
end
def remove_container
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
end
def remove_image
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
end
def cleanup_traefik
chain \
docker(:container, :stop, "traefik"),
combine(
docker(:container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"),
docker(:image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik")
)
end
def ensure_proxy_directory
make_directory config.proxy_boot.host_directory
end
def remove_proxy_directory
remove_directory config.proxy_boot.host_directory
end
def ensure_apps_config_directory
make_directory config.proxy_boot.apps_directory
end
def boot_config
[ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ]
end
def read_boot_options
read_file(config.proxy_boot.options_file, default: config.proxy_boot.default_boot_options.join(" "))
end
def read_image
read_file(config.proxy_boot.image_file, default: config.proxy_boot.image_default)
end
def read_image_version
read_file(config.proxy_boot.image_version_file, default: Kamal::Configuration::Proxy::Run::MINIMUM_VERSION)
end
def read_run_command
read_file(config.proxy_boot.run_command_file)
end
def reset_boot_options
remove_file config.proxy_boot.options_file
end
def reset_image
remove_file config.proxy_boot.image_file
end
def reset_image_version
remove_file config.proxy_boot.image_version_file
end
def reset_run_command
remove_file config.proxy_boot.run_command_file
end
private
def container_name
config.proxy_boot.container_name
end
def read_file(file, default: nil)
combine [ :cat, file, "2>", "/dev/null" ], [ :echo, "\"#{default}\"" ], by: "||"
end
def docker_run
docker \
:run,
"--name", container_name,
"--network", "kamal",
"--detach",
"--restart", "unless-stopped",
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
*config.proxy_boot.apps_volume.docker_args
end
end
================================================
FILE: lib/kamal/commands/prune.rb
================================================
require "active_support/duration"
require "active_support/core_ext/numeric/time"
class Kamal::Commands::Prune < Kamal::Commands::Base
def dangling_images
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}"
end
def tagged_images
pipe \
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
grep("-v -w \"#{active_image_list}\""),
"while read image tag; do docker rmi $tag; done"
end
def app_containers(retain:)
pipe \
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
"tail -n +#{retain + 1}",
"while read container_id; do docker rm $container_id; done"
end
private
def stopped_containers_filters
[ "created", "exited", "dead" ].flat_map { |status| [ "--filter", "status=#{status}" ] }
end
def active_image_list
# Pull the images that are used by any containers
# Append repo:latest - to avoid deleting the latest tag
# Append repo:<none> - to avoid deleting dangling images that are in use. Unused dangling images are deleted separately
"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=#{config.service} | tr -d '\\n')#{config.latest_image}\\|#{config.repository}:<none>"
end
def service_filter
[ "--filter", "label=service=#{config.service}" ]
end
end
================================================
FILE: lib/kamal/commands/registry.rb
================================================
class Kamal::Commands::Registry < Kamal::Commands::Base
def login(registry_config: nil)
registry_config ||= config.registry
return if registry_config.local?
docker :login,
registry_config.server,
"-u", sensitive(Kamal::Utils.escape_shell_value(registry_config.username)),
"-p", sensitive(Kamal::Utils.escape_shell_value(registry_config.password))
end
def logout(registry_config: nil)
registry_config ||= config.registry
docker :logout, registry_config.server
end
def setup(registry_config: nil)
registry_config ||= config.registry
combine \
docker(:start, "kamal-docker-registry"),
docker(:run, "--detach", "-p", "127.0.0.1:#{registry_config.local_port}:5000", "--name", "kamal-docker-registry", "registry:3"),
by: "||"
end
def remove
combine \
docker(:stop, "kamal-docker-registry"),
docker(:rm, "kamal-docker-registry"),
by: "&&"
end
def local?
config.registry.local?
end
end
================================================
FILE: lib/kamal/commands/server.rb
================================================
class Kamal::Commands::Server < Kamal::Commands::Base
def ensure_run_directory
make_directory config.run_directory
end
def remove_app_directory
remove_directory config.app_directory
end
def app_directory_count
pipe \
[ :ls, config.apps_directory ],
[ :wc, "-l" ]
end
end
================================================
FILE: lib/kamal/commands.rb
================================================
module Kamal::Commands
end
================================================
FILE: lib/kamal/configuration/accessory.rb
================================================
class Kamal::Configuration::Accessory
include Kamal::Configuration::Validation
DEFAULT_NETWORK = "kamal"
delegate :argumentize, :optionize, to: Kamal::Utils
attr_reader :name, :env, :proxy, :registry
def initialize(name, config:)
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
validate! \
accessory_config,
example: validation_yml["accessories"]["mysql"],
context: "accessories/#{name}",
with: Kamal::Configuration::Validator::Accessory
ensure_valid_roles
@env = initialize_env
@proxy = initialize_proxy if running_proxy?
@registry = initialize_registry if accessory_config["registry"].present?
end
def service_name
accessory_config["service"] || "#{config.service}-#{name}"
end
def image
[ registry&.server, accessory_config["image"] ].compact.join("/")
end
def hosts
hosts_from_host || hosts_from_hosts || hosts_from_roles || hosts_from_tags
end
def port
if port = accessory_config["port"]&.to_s
port.include?(":") ? port : "#{port}:#{port}"
end
end
def network_args
argumentize "--network", network
end
def publish_args
argumentize "--publish", port if port
end
def labels
default_labels.merge(accessory_config["labels"] || {})
end
def label_args
argumentize "--label", labels
end
def env_args
[ *env.clear_args, *argumentize("--env-file", secrets_path) ]
end
def env_directory
File.join(config.env_directory, "accessories")
end
def secrets_io
env.secrets_io
end
def secrets_path
File.join(config.env_directory, "accessories", "#{name}.env")
end
def files
accessory_config["files"]&.to_h do |config|
parse_path_config(config, default_mode: "755") do |local, remote|
{
key: expand_local_file(local),
host_path: expand_remote_file(remote),
container_path: remote
}
end
end || {}
end
def directories
accessory_config["directories"]&.to_h do |config|
parse_path_config(config, default_mode: nil) do |local, remote|
{
key: expand_host_path(local),
host_path: expand_host_path_for_volume(local),
container_path: remote
}
end
end || {}
end
def volume_args
argumentize("--volume", specific_volumes) + (path_volumes(files) + path_volumes(directories)).flat_map(&:docker_args)
end
def option_args
if args = accessory_config["options"]
optionize args
else
[]
end
end
def cmd
accessory_config["cmd"]
end
def running_proxy?
accessory_config["proxy"].present?
end
private
attr_reader :config, :accessory_config
def initialize_env
Kamal::Configuration::Env.new \
config: accessory_config.fetch("env", {}),
secrets: config.secrets,
context: "accessories/#{name}/env"
end
def initialize_proxy
Kamal::Configuration::Proxy.new \
config: config,
proxy_config: accessory_config["proxy"],
context: "accessories/#{name}/proxy",
secrets: config.secrets
end
def initialize_registry
Kamal::Configuration::Registry.new \
config: accessory_config,
secrets: config.secrets,
context: "accessories/#{name}/registry"
end
def default_labels
{ "service" => service_name }
end
def expand_local_file(local_file)
if local_file.end_with?("erb")
with_env_loaded { read_dynamic_file(local_file) }
else
Pathname.new(File.expand_path(local_file)).to_s
end
end
def with_env_loaded
env.to_h.each { |k, v| ENV[k] = v }
yield
ensure
env.to_h.each { |k, v| ENV.delete(k) }
end
def read_dynamic_file(local_file)
StringIO.new(ERB.new(File.read(local_file)).result)
end
def expand_remote_file(remote_file)
service_name + remote_file
end
def specific_volumes
accessory_config["volumes"] || []
end
def path_volumes(paths)
paths.map do |local, config|
Kamal::Configuration::Volume.new \
host_path: config[:host_path],
container_path: config[:container_path],
options: config[:options]
end
end
def parse_path_config(config, default_mode:)
if config.is_a?(Hash)
local, remote = config["local"], config["remote"]
expanded = yield(local, remote)
[
expanded[:key],
expanded.except(:key).merge(
options: config["options"],
mode: config["mode"] || default_mode,
owner: config["owner"]
)
]
else
local, remote, options = config.split(":", 3)
expanded = yield(local, remote)
[
expanded[:key],
expanded.except(:key).merge(
options: options,
mode: default_mode,
owner: nil
)
]
end
end
def expand_host_path(host_path)
absolute_path?(host_path) ? host_path : File.join(service_data_directory, host_path)
end
def expand_host_path_for_volume(host_path)
absolute_path?(host_path) ? host_path : File.join(service_name, host_path)
end
def absolute_path?(path)
Pathname.new(path).absolute?
end
def service_data_directory
"$PWD/#{service_name}"
end
def hosts_from_host
[ accessory_config["host"] ] if accessory_config.key?("host")
end
def hosts_from_hosts
accessory_config["hosts"] if accessory_config.key?("hosts")
end
def hosts_from_roles
if accessory_config.key?("role")
config.role(accessory_config["role"])&.hosts
elsif accessory_config.key?("roles")
accessory_config["roles"].flat_map { |role| config.role(role)&.hosts }
end
end
def hosts_from_tags
if accessory_config.key?("tag")
extract_hosts_from_config_with_tag(accessory_config["tag"])
elsif accessory_config.key?("tags")
accessory_config["tags"].flat_map { |tag| extract_hosts_from_config_with_tag(tag) }
end
end
def extract_hosts_from_config_with_tag(tag)
if (servers_with_roles = config.raw_config.servers).is_a?(Hash)
servers_with_roles.flat_map do |role, servers_in_role|
servers_in_role.filter_map do |host|
host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag)
end
end
end
end
def network
accessory_config["network"] || DEFAULT_NETWORK
end
def ensure_valid_roles
if accessory_config["roles"] && (missing_roles = accessory_config["roles"] - config.roles.map(&:name)).any?
raise Kamal::ConfigurationError, "accessories/#{name}: unknown roles #{missing_roles.join(", ")}"
elsif accessory_config["role"] && !config.role(accessory_config["role"])
raise Kamal::ConfigurationError, "accessories/#{name}: unknown role #{accessory_config["role"]}"
end
end
end
================================================
FILE: lib/kamal/configuration/alias.rb
================================================
class Kamal::Configuration::Alias
include Kamal::Configuration::Validation
attr_reader :name, :command
def initialize(name, config:)
@name, @command = name.inquiry, config.raw_config["aliases"][name]
validate! \
command,
example: validation_yml["aliases"]["uname"],
context: "aliases/#{name}",
with: Kamal::Configuration::Validator::Alias
end
end
================================================
FILE: lib/kamal/configuration/boot.rb
================================================
class Kamal::Configuration::Boot
include Kamal::Configuration::Validation
attr_reader :boot_config, :host_count
def initialize(config:)
@boot_config = config.raw_config.boot || {}
@host_count = config.all_hosts.count
validate! boot_config
end
def limit
limit = boot_config["limit"]
if limit.to_s.end_with?("%")
[ host_count * limit.to_i / 100, 1 ].max
else
limit
end
end
def wait
boot_config["wait"]
end
def parallel_roles
boot_config["parallel_roles"]
end
end
================================================
FILE: lib/kamal/configuration/builder.rb
================================================
class Kamal::Configuration::Builder
include Kamal::Configuration::Validation
attr_reader :config, :builder_config
delegate :image, :service, to: :config
delegate :server, to: :"config.registry"
def initialize(config:)
@config = config
@builder_config = config.raw_config.builder || {}
@image = config.image
@server = config.registry.server
@service = config.service
validate! builder_config, with: Kamal::Configuration::Validator::Builder
end
def to_h
builder_config
end
def remote
builder_config["remote"]
end
def arches
Array(builder_config.fetch("arch", default_arch))
end
def local_arches
@local_arches ||= if local_disabled?
[]
elsif remote
arches & [ Kamal::Utils.docker_arch ]
else
arches
end
end
def remote_arches
@remote_arches ||= if remote
arches - local_arches
else
[]
end
end
def remote?
remote_arches.any?
end
def local?
!local_disabled? && (arches.empty? || local_arches.any?)
end
def cloud?
driver.start_with? "cloud"
end
def cached?
!!builder_config["cache"]
end
def pack?
!!builder_config["pack"]
end
def args
builder_config["args"] || {}
end
def secrets
(builder_config["secrets"] || []).to_h { |key| [ key, config.secrets[key] ] }
end
def dockerfile
builder_config["dockerfile"] || "Dockerfile"
end
def target
builder_config["target"]
end
def context
builder_config["context"] || "."
end
def driver
builder_config.fetch("driver", "docker-container")
end
def pack_builder
builder_config["pack"]["builder"] if pack?
end
def pack_buildpacks
builder_config["pack"]["buildpacks"] if pack?
end
def local_disabled?
builder_config["local"] == false
end
def cache_from
if cached?
case builder_config["cache"]["type"]
when "gha"
cache_from_config_for_gha
when "registry"
cache_from_config_for_registry
end
end
end
def cache_to
if cached?
case builder_config["cache"]["type"]
when "gha"
cache_to_config_for_gha
when "registry"
cache_to_config_for_registry
end
end
end
def ssh
builder_config["ssh"]
end
def provenance
builder_config["provenance"]
end
def sbom
builder_config["sbom"]
end
def git_clone?
Kamal::Git.used? && builder_config["context"].nil?
end
def clone_directory
@clone_directory ||= File.join Dir.tmpdir, "kamal-clones", [ service, pwd_sha ].compact.join("-")
end
def build_directory
@build_directory ||=
if git_clone?
File.join clone_directory, repo_basename, repo_relative_pwd
else
"."
end
end
def docker_driver?
driver == "docker"
end
private
def valid?
if docker_driver?
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support remote builders" if remote
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support caching" if cached?
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support multiple arches" if arches.many?
end
if @options["cache"] && @options["cache"]["type"]
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
end
end
def cache_image
builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache"
end
def cache_image_ref
[ server, cache_image ].compact.join("/")
end
def cache_options
builder_config["cache"]&.fetch("options", nil)
end
def cache_from_config_for_gha
individual_options = cache_options&.split(",") || []
allowed_options = individual_options.select { |option| option =~ /^(url|url_v2|token|scope|timeout)=/ }
[ "type=gha", *allowed_options ].compact.join(",")
end
def cache_from_config_for_registry
[ "type=registry", "ref=#{cache_image_ref}" ].compact.join(",")
end
def cache_to_config_for_gha
[ "type=gha", cache_options ].compact.join(",")
end
def cache_to_config_for_registry
[ "type=registry", "ref=#{cache_image_ref}", cache_options ].compact.join(",")
end
def repo_basename
File.basename(Kamal::Git.root)
end
def repo_relative_pwd
Dir.pwd.delete_prefix(Kamal::Git.root)
end
def pwd_sha
Digest::SHA256.hexdigest(Dir.pwd)[0..12]
end
def default_arch
docker_driver? ? [] : [ "amd64", "arm64" ]
end
end
================================================
FILE: lib/kamal/configuration/docs/accessory.yml
================================================
# Accessories
#
# Accessories can be booted on a single host, a list of hosts, or on specific roles.
# The hosts do not need to be defined in the Kamal servers configuration.
#
# Accessories are managed separately from the main service — they are not updated
# when you deploy, and they do not have zero-downtime deployments.
#
# Run `kamal accessory boot <accessory>` to boot an accessory.
# See `kamal accessory --help` for more information.
# Configuring accessories
#
# First, define the accessory in the `accessories`:
accessories:
mysql:
# Service name
#
# This is used in the service label and defaults to `<service>-<accessory>`,
# where `<service>` is the main service name from the root configuration:
service: mysql
# Image
#
# The Docker image to use.
# Prefix it with its server when using root level registry different from Docker Hub.
# Define registry directly or via anchors when it differs from root level registry.
image: mysql:8.0
# Registry
#
# By default accessories use Docker Hub registry.
# You can specify different registry per accessory with this option.
# Don't prefix image with this registry server.
# Use anchors if you need to set the same specific registry for several accessories.
#
# ```yml
# registry:
# <<: *specific-registry
# ```
#
# See kamal docs registry for more information:
registry:
...
# Accessory hosts
#
# Specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`:
host: mysql-db1
hosts:
- mysql-db1
- mysql-db2
role: mysql
roles:
- mysql
tag: writer
tags:
- writer
- reader
# Custom command
#
# You can set a custom command to run in the container if you do not want to use the default:
cmd: "bin/mysqld"
# Port mappings
#
# See [https://docs.docker.com/network/](https://docs.docker.com/network/), and
# especially note the warning about the security implications of exposing ports publicly.
port: "127.0.0.1:3306:3306"
# Labels
labels:
app: myapp
# Options
#
# These are passed to the Docker run command in the form `--<name> <value>`:
options:
restart: always
cpus: 2
# Environment variables
#
# See kamal docs env for more information:
env:
...
# Copying files
#
# You can specify files to mount into the container.
#
# They will be uploaded from the local repo to the host and then mounted.
# ERB files will be evaluated before being copied.
#
# You can use the string format: `local:remote` or `local:remote:options`
# where the options can be `ro` for read-only or `z`/`Z` for SELinux labels
files:
- config/my.cnf.erb:/etc/mysql/my.cnf
- config/myoptions.cnf:/etc/mysql/myoptions.cnf:ro
- config/certs:/etc/mysql/certs:ro,Z
#
# Or you can use the hash format for custom mode and ownership.
#
# Note: Setting `owner` requires root access:
files:
- local: config/secret.key
remote: /etc/mysql/secret.key
mode: "0600"
owner: "mysql:mysql"
- local: config/ca-cert.pem
remote: /etc/mysql/certs/ca-cert.pem
mode: "0644"
owner: "1000:1000"
options: "Z"
# Directories
#
# You can specify directories to mount into the container. They will be created on the host
# before being mounted.
#
# You can use the string format: `local:remote` or `local:remote:options`
# where the options can be `ro` for read-only or `z`/`Z` for SELinux labels
directories:
- mysql-logs:/var/log/mysql
- mysql-data:/var/lib/mysql:z
#
# Or you can use the hash format for custom mode and ownership.
#
# Note: Setting `owner` requires root access:
directories:
- local: mysql-data
remote: /var/lib/mysql
mode: "0750"
owner: "mysql:mysql"
- local: mysql-logs
remote: /var/log/mysql
mode: "0755"
options: "z"
# Volumes
#
# Any other volumes to mount, in addition to the files and directories.
# They are not created or copied before mounting:
volumes:
- /path/to/mysql-logs:/var/log/mysql
# Network
#
# The network the accessory will be attached to.
#
# Defaults to kamal:
network: custom
# Proxy
#
# You can run your accessory behind the Kamal proxy. See kamal docs proxy for more information
proxy:
...
================================================
FILE: lib/kamal/configuration/docs/alias.yml
================================================
# Aliases
#
# Aliases are shortcuts for Kamal commands.
#
# For example, for a Rails app, you might open a console with:
#
# ```shell
# kamal app exec -i --reuse "bin/rails console"
# ```
#
# By defining an alias, like this:
aliases:
console: app exec -i --reuse "bin/rails console"
# You can now open the console with:
#
# ```shell
# kamal console
# ```
# Configuring aliases
#
# Aliases are defined in the root config under the alias key.
#
# Each alias is named and can only contain lowercase letters, numbers, dashes, and underscores:
aliases:
uname: app exec -p -q -r web "uname -a"
#
# Aliases can include a destination with the `-d` flag:
staging_deploy: deploy -d staging
================================================
FILE: lib/kamal/configuration/docs/boot.yml
================================================
# Booting
#
# When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
#
# Kamal’s default is to boot new containers on all hosts in parallel. However, you can control this with the boot configuration.
boot:
# The number or percentage of hosts to boot at a time.
# This can be an integer (e.g., 3) or a percentage string (e.g., 25%).
limit: 25%
# The number of seconds to wait between booting each group of hosts.
wait: 10
# Whether to boot roles in parallel on a host.
#
# If a host has multiple roles, control whether they are booted in parallel or sequentially on that host.
#
# Defaults to false.
parallel_roles: true
================================================
FILE: lib/kamal/configuration/docs/builder.yml
================================================
# Builder
#
# The builder configuration controls how the application is built with `docker build`.
#
# See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information.
# Builder options
#
# Options go under the builder key in the root configuration.
builder:
# Arch
#
# The architectures to build for — you can set an array or just a single value.
#
# Allowed values are `amd64` and `arm64`:
arch:
- amd64
# Remote
#
# The connection string for a remote builder. If supplied, Kamal will use this
# for builds that do not match the local architecture of the deployment host.
remote: ssh://docker@docker-builder
# Local
#
# If set to false, Kamal will always use the remote builder even when building
# the local architecture.
#
# Defaults to true:
local: true
# Buildpack configuration
#
# The build configuration for using pack to build a Cloud Native Buildpack image.
#
# For additional buildpack customization options you can create a project descriptor
# file(project.toml) that the Pack CLI will automatically use.
# See https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/use-project-toml/ for more information.
pack:
builder: heroku/builder:24
buildpacks:
- heroku/ruby
- heroku/procfile
# Builder cache
#
# The type must be either 'gha' or 'registry'.
#
# The image is only used for registry cache and is not compatible with the Docker driver:
cache:
type: registry
options: mode=max
image: kamal-app-build-cache
# Build context
#
# If this is not set, then a local Git clone of the repo is used.
# This ensures a clean build with no uncommitted changes.
#
# To use the local checkout instead, you can set the context to `.`, or a path to another directory.
context: .
# Dockerfile
#
# The Dockerfile to use for building, defaults to `Dockerfile`:
dockerfile: Dockerfile.production
# Build target
#
# If not set, then the default target is used:
target: production
# Build arguments
#
# Any additional build arguments, passed to `docker build` with `--build-arg <key>=<value>`:
args:
ENVIRONMENT: production
# Referencing build arguments
#
# ```shell
# ARG RUBY_VERSION
# FROM ruby:$RUBY_VERSION-slim as base
# ```
# Build secrets
#
# Values are read from `.kamal/secrets`:
secrets:
- SECRET1
- SECRET2
# Referencing build secrets
#
# ```shell
# # Copy Gemfiles
# COPY Gemfile Gemfile.lock ./
#
# # Install dependencies, including private repositories via access token
# # Then remove bundle cache with exposed GITHUB_TOKEN
# RUN --mount=type=secret,id=GITHUB_TOKEN \
# BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
# bundle install && \
# rm -rf /usr/local/bundle/cache
# ```
# SSH
#
# SSH agent socket or keys to expose to the build:
ssh: default=$SSH_AUTH_SOCK
# Driver
#
# The build driver to use, defaults to `docker-container`:
driver: docker
#
# If you want to use Docker Build Cloud (https://www.docker.com/products/build-cloud/), you can set the driver to:
driver: cloud org-name/builder-name
# Provenance
#
# It is used to configure provenance attestations for the build result.
# The value can also be a boolean to enable or disable provenance attestations.
provenance: mode=max
# SBOM (Software Bill of Materials)
#
# It is used to configure SBOM generation for the build result.
# The value can also be a boolean to enable or disable SBOM generation.
sbom: true
================================================
FILE: lib/kamal/configuration/docs/configuration.yml
================================================
# Kamal Configuration
#
# Configuration is read from the `config/deploy.yml`.
# Destinations
#
# When running commands, you can specify a destination with the `-d` flag,
# e.g., `kamal deploy -d staging`.
#
# In this case, the configuration will also be read from `config/deploy.staging.yml`
# and merged with the base configuration.
# Extensions
#
# Kamal will not accept unrecognized keys in the configuration file.
#
# However, you might want to declare a configuration block using YAML anchors
# and aliases to avoid repetition.
#
# You can prefix a configuration section with `x-` to indicate that it is an
# extension. Kamal will ignore the extension and not raise an error.
# The service name
#
# This is a required value. It is used as the container name prefix.
service: myapp
# The Docker image name
#
# The image will be pushed to the configured registry.
image: my-image
# Labels
#
# Additional labels to add to the container:
labels:
my-label: my-value
# Volumes
#
# Additional volumes to mount into the container:
volumes:
- /path/on/host:/path/in/container:ro
# Registry
#
# The Docker registry configuration, see kamal docs registry:
registry:
...
# Servers
#
# The servers to deploy to, optionally with custom roles, see kamal docs servers:
servers:
...
# Environment variables
#
# See kamal docs env:
env:
...
# Asset path
#
# Used for asset bridging across deployments, default to `nil`.
#
# If there are changes to CSS or JS files, we may get requests
# for the old versions on the new container, and vice versa.
#
# To avoid 404s, we can specify an asset path.
# Kamal will replace that path in the container with a mapped
# volume containing both sets of files.
# This requires that file names change when the contents change
# (e.g., by including a hash of the contents in the name).
#
# To configure this, set the path to the assets.
#
# You can also specify mount options after a colon, such as `ro` for read-only
# or `z`/`Z` for SELinux labels
asset_path: /path/to/assets
# Hooks path
#
# Path to hooks, defaults to `.kamal/hooks`.
# See https://kamal-deploy.org/docs/hooks for more information:
hooks_path: /user_home/kamal/hooks
# Hook output
#
# Hook output visibility. Can be set globally or per-hook.
# CLI flags (`-v`, `-q`) override these settings.
#
# - `:quiet` - hook output is hidden
# - `:verbose` - hook output is shown
#
# With no setting, hook output follows CLI verbosity flags.
#
# Note: Failed hooks always show output in the error message regardless of setting.
#
# Global setting for all hooks:
hooks_output: :verbose
# Or per-hook settings:
hooks_output:
pre-deploy: :verbose
pre-build: :quiet
# Secrets path
#
# Path to secrets, defaults to `.kamal/secrets`.
# Kamal will look for `<secrets_path>-common` and `<secrets_path>` (or `<secrets_path>.<destination>` when using destinations):
secrets_path: /user_home/kamal/secrets
# Error pages
#
# A directory relative to the app root to find error pages for the proxy to serve.
# Any files in the format 4xx.html or 5xx.html will be copied to the hosts.
error_pages_path: public
# Require destinations
#
# Whether deployments require a destination to be specified, defaults to `false`:
require_destination: true
# Primary role
#
# This defaults to `web`, but if you have no web role, you can change this:
primary_role: workers
# Allowing empty roles
#
# Whether roles with no servers are allowed. Defaults to `false`:
allow_empty_roles: false
# Retain containers
#
# How many old containers and images we retain, defaults to 5:
retain_containers: 3
# Minimum version
#
# The minimum version of Kamal required to deploy this configuration, defaults to `nil`:
minimum_version: 1.3.0
# Readiness delay
#
# Seconds to wait for a container to boot after it is running, default 7.
#
# This only applies to containers that do not run a proxy or specify a healthcheck:
readiness_delay: 4
# Deploy timeout
#
# How long to wait for a container to become ready, default 30:
deploy_timeout: 10
# Drain timeout
#
# How long to wait for a container to drain, default 30:
drain_timeout: 10
# Run directory
#
# Directory to store kamal runtime files in on the host, default `.kamal`:
run_directory: /etc/kamal
# SSH options
#
# See kamal docs ssh:
ssh:
...
# Builder options
#
# See kamal docs builder:
builder:
...
# Accessories
#
# Additional services to run in Docker, see kamal docs accessory:
accessories:
...
# Proxy
#
# Configuration for kamal-proxy, see kamal docs proxy:
proxy:
...
# SSHKit
#
# See kamal docs sshkit:
sshkit:
...
# Boot options
#
# See kamal docs boot:
boot:
...
# Logging
#
# Docker logging configuration, see kamal docs logging:
logging:
...
# Aliases
#
# Alias configuration, see kamal docs alias:
aliases:
...
================================================
FILE: lib/kamal/configuration/docs/env.yml
================================================
# Environment variables
#
# Environment variables can be set directly in the Kamal configuration or
# read from `.kamal/secrets`.
# Reading environment variables from the configuration
#
# Environment variables can be set directly in the configuration file.
#
# These are passed to the `docker run` command when deploying.
env:
DATABASE_HOST: mysql-db1
DATABASE_PORT: 3306
# Secrets
#
# Kamal uses dotenv to automatically load environment variables set in the `.kamal/secrets` file.
#
# If you are using destinations, secrets will instead be read from `.kamal/secrets.<DESTINATION>` if
# it exists.
#
# Common secrets across all destinations can be set in `.kamal/secrets-common`.
#
# This file can be used to set variables like `KAMAL_REGISTRY_PASSWORD` or database passwords.
# You can use variable or command substitution in the secrets file.
#
# ```shell
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
# RAILS_MASTER_KEY=$(cat config/master.key)
# ```
#
# You can also use [secret helpers](../../commands/secrets) for some common password managers.
#
# ```shell
# SECRETS=$(kamal secrets fetch ...)
#
# REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
# DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
# ```
#
# If you store secrets directly in `.kamal/secrets`, ensure that it is not checked into version control.
#
# To pass the secrets, you should list them under the `secret` key. When you do this, the
# other variables need to be moved under the `clear` key.
#
# Unlike clear values, secrets are not passed directly to the container
# but are stored in an env file on the host:
env:
clear:
DB_USER: app
secret:
- DB_PASSWORD
# Aliased secrets
#
# You can also alias secrets to other secrets using a `:` separator.
#
# This is useful when the ENV name is different from the secret name. For example, if you have two
# places where you need to define the ENV variable `DB_PASSWORD`, but the value is different depending
# on the context.
#
# ```shell
# SECRETS=$(kamal secrets fetch ...)
#
# MAIN_DB_PASSWORD=$(kamal secrets extract MAIN_DB_PASSWORD $SECRETS)
# SECONDARY_DB_PASSWORD=$(kamal secrets extract SECONDARY_DB_PASSWORD $SECRETS)
# ```
env:
secret:
- DB_PASSWORD:MAIN_DB_PASSWORD
tags:
secondary_db:
secret:
- DB_PASSWORD:SECONDARY_DB_PASSWORD
accessories:
main_db_accessory:
env:
secret:
- DB_PASSWORD:MAIN_DB_PASSWORD
secondary_db_accessory:
env:
secret:
- DB_PASSWORD:SECONDARY_DB_PASSWORD
# Tags
#
# Tags are used to add extra env variables to specific hosts.
# See kamal docs servers for how to tag hosts.
#
# Tags are only allowed in the top-level env configuration (i.e., not under a role-specific env).
#
# The env variables can be specified with secret and clear values as explained above.
env:
tags:
<tag1>:
MYSQL_USER: monitoring
<tag2>:
clear:
MYSQL_USER: readonly
secret:
- MYSQL_PASSWORD
# Example configuration
env:
clear:
MYSQL_USER: app
secret:
- MYSQL_PASSWORD
tags:
monitoring:
MYSQL_USER: monitoring
replica:
clear:
MYSQL_USER: readonly
secret:
- READONLY_PASSWORD
================================================
FILE: lib/kamal/configuration/docs/logging.yml
================================================
# Custom logging configuration
#
# Set these to control the Docker logging driver and options.
# Logging settings
#
# These go under the logging key in the configuration file.
#
# This can be specified at the root level or for a specific role.
logging:
# Driver
#
# The logging driver to use, passed to Docker via `--log-driver`:
driver: json-file
# Options
#
# Any logging options to pass to the driver, passed to Docker via `--log-opt`:
options:
max-size: 100m
================================================
FILE: lib/kamal/configuration/docs/proxy.yml
================================================
# Proxy
#
# Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to provide
# gapless deployments. It runs on ports 80 and 443 and forwards req
gitextract_hqeups_8/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── docker-publish.yml
├── .gitignore
├── .rubocop.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── MIT-LICENSE
├── README.md
├── bin/
│ ├── docs
│ ├── kamal
│ ├── release
│ └── test
├── gemfiles/
│ └── rails_edge.gemfile
├── kamal.gemspec
├── lib/
│ ├── kamal/
│ │ ├── cli/
│ │ │ ├── accessory.rb
│ │ │ ├── alias/
│ │ │ │ └── command.rb
│ │ │ ├── app/
│ │ │ │ ├── assets.rb
│ │ │ │ ├── boot.rb
│ │ │ │ ├── error_pages.rb
│ │ │ │ └── ssl_certificates.rb
│ │ │ ├── app.rb
│ │ │ ├── base.rb
│ │ │ ├── build/
│ │ │ │ ├── clone.rb
│ │ │ │ └── port_forwarding.rb
│ │ │ ├── build.rb
│ │ │ ├── healthcheck/
│ │ │ │ ├── barrier.rb
│ │ │ │ ├── error.rb
│ │ │ │ └── poller.rb
│ │ │ ├── lock.rb
│ │ │ ├── main.rb
│ │ │ ├── proxy.rb
│ │ │ ├── prune.rb
│ │ │ ├── registry.rb
│ │ │ ├── secrets.rb
│ │ │ ├── server.rb
│ │ │ └── templates/
│ │ │ ├── deploy.yml
│ │ │ ├── sample_hooks/
│ │ │ │ ├── docker-setup.sample
│ │ │ │ ├── post-app-boot.sample
│ │ │ │ ├── post-deploy.sample
│ │ │ │ ├── post-proxy-reboot.sample
│ │ │ │ ├── pre-app-boot.sample
│ │ │ │ ├── pre-build.sample
│ │ │ │ ├── pre-connect.sample
│ │ │ │ ├── pre-deploy.sample
│ │ │ │ └── pre-proxy-reboot.sample
│ │ │ └── secrets
│ │ ├── cli.rb
│ │ ├── commander/
│ │ │ └── specifics.rb
│ │ ├── commander.rb
│ │ ├── commands/
│ │ │ ├── accessory/
│ │ │ │ └── proxy.rb
│ │ │ ├── accessory.rb
│ │ │ ├── app/
│ │ │ │ ├── assets.rb
│ │ │ │ ├── containers.rb
│ │ │ │ ├── error_pages.rb
│ │ │ │ ├── execution.rb
│ │ │ │ ├── images.rb
│ │ │ │ ├── logging.rb
│ │ │ │ └── proxy.rb
│ │ │ ├── app.rb
│ │ │ ├── auditor.rb
│ │ │ ├── base.rb
│ │ │ ├── builder/
│ │ │ │ ├── base.rb
│ │ │ │ ├── clone.rb
│ │ │ │ ├── cloud.rb
│ │ │ │ ├── hybrid.rb
│ │ │ │ ├── local.rb
│ │ │ │ ├── pack.rb
│ │ │ │ └── remote.rb
│ │ │ ├── builder.rb
│ │ │ ├── docker.rb
│ │ │ ├── hook.rb
│ │ │ ├── lock.rb
│ │ │ ├── proxy.rb
│ │ │ ├── prune.rb
│ │ │ ├── registry.rb
│ │ │ └── server.rb
│ │ ├── commands.rb
│ │ ├── configuration/
│ │ │ ├── accessory.rb
│ │ │ ├── alias.rb
│ │ │ ├── boot.rb
│ │ │ ├── builder.rb
│ │ │ ├── docs/
│ │ │ │ ├── accessory.yml
│ │ │ │ ├── alias.yml
│ │ │ │ ├── boot.yml
│ │ │ │ ├── builder.yml
│ │ │ │ ├── configuration.yml
│ │ │ │ ├── env.yml
│ │ │ │ ├── logging.yml
│ │ │ │ ├── proxy.yml
│ │ │ │ ├── registry.yml
│ │ │ │ ├── role.yml
│ │ │ │ ├── servers.yml
│ │ │ │ ├── ssh.yml
│ │ │ │ └── sshkit.yml
│ │ │ ├── env/
│ │ │ │ └── tag.rb
│ │ │ ├── env.rb
│ │ │ ├── logging.rb
│ │ │ ├── proxy/
│ │ │ │ ├── boot.rb
│ │ │ │ └── run.rb
│ │ │ ├── proxy.rb
│ │ │ ├── registry.rb
│ │ │ ├── role.rb
│ │ │ ├── servers.rb
│ │ │ ├── ssh.rb
│ │ │ ├── sshkit.rb
│ │ │ ├── validation.rb
│ │ │ ├── validator/
│ │ │ │ ├── accessory.rb
│ │ │ │ ├── alias.rb
│ │ │ │ ├── builder.rb
│ │ │ │ ├── configuration.rb
│ │ │ │ ├── env.rb
│ │ │ │ ├── proxy.rb
│ │ │ │ ├── registry.rb
│ │ │ │ ├── role.rb
│ │ │ │ └── servers.rb
│ │ │ ├── validator.rb
│ │ │ └── volume.rb
│ │ ├── configuration.rb
│ │ ├── docker.rb
│ │ ├── env_file.rb
│ │ ├── git.rb
│ │ ├── secrets/
│ │ │ ├── adapters/
│ │ │ │ ├── aws_secrets_manager.rb
│ │ │ │ ├── base.rb
│ │ │ │ ├── bitwarden.rb
│ │ │ │ ├── bitwarden_secrets_manager.rb
│ │ │ │ ├── doppler.rb
│ │ │ │ ├── enpass.rb
│ │ │ │ ├── gcp_secret_manager.rb
│ │ │ │ ├── last_pass.rb
│ │ │ │ ├── one_password.rb
│ │ │ │ ├── passbolt.rb
│ │ │ │ └── test.rb
│ │ │ ├── adapters.rb
│ │ │ └── dotenv/
│ │ │ └── inline_command_substitution.rb
│ │ ├── secrets.rb
│ │ ├── sshkit_with_ext.rb
│ │ ├── tags.rb
│ │ ├── utils/
│ │ │ └── sensitive.rb
│ │ ├── utils.rb
│ │ └── version.rb
│ └── kamal.rb
└── test/
├── cli/
│ ├── accessory_test.rb
│ ├── app_test.rb
│ ├── build_test.rb
│ ├── cli_test_case.rb
│ ├── lock_test.rb
│ ├── main_test.rb
│ ├── proxy_test.rb
│ ├── prune_test.rb
│ ├── registry_test.rb
│ ├── secrets_test.rb
│ └── server_test.rb
├── commander_test.rb
├── commands/
│ ├── accessory_test.rb
│ ├── app_test.rb
│ ├── auditor_test.rb
│ ├── builder_test.rb
│ ├── docker_test.rb
│ ├── hook_test.rb
│ ├── lock_test.rb
│ ├── proxy_test.rb
│ ├── prune_test.rb
│ ├── registry_test.rb
│ └── server_test.rb
├── configuration/
│ ├── accessory_test.rb
│ ├── boot_test.rb
│ ├── builder_test.rb
│ ├── env/
│ │ └── tags_test.rb
│ ├── env_test.rb
│ ├── proxy/
│ │ └── boot_test.rb
│ ├── proxy_test.rb
│ ├── role_test.rb
│ ├── ssh_test.rb
│ ├── sshkit_test.rb
│ ├── validation_test.rb
│ └── volume_test.rb
├── configuration_test.rb
├── env_file_test.rb
├── fixtures/
│ ├── deploy.elsewhere.yml
│ ├── deploy.erb.yml
│ ├── deploy.yml
│ ├── deploy2.yml
│ ├── deploy_for_dest.mars.yml
│ ├── deploy_for_dest.world.yml
│ ├── deploy_for_dest.yml
│ ├── deploy_for_required_dest.world.yml
│ ├── deploy_for_required_dest.yml
│ ├── deploy_primary_web_role_override.yml
│ ├── deploy_simple.yml
│ ├── deploy_with_accessories.yml
│ ├── deploy_with_accessories_on_independent_server.yml
│ ├── deploy_with_accessories_with_different_registries.yml
│ ├── deploy_with_aliases.yml
│ ├── deploy_with_assets.yml
│ ├── deploy_with_boot_strategy.yml
│ ├── deploy_with_cloud_builder.yml
│ ├── deploy_with_env_tags.yml
│ ├── deploy_with_error_pages.yml
│ ├── deploy_with_extensions.yml
│ ├── deploy_with_hybrid_builder.yml
│ ├── deploy_with_local_registry.yml
│ ├── deploy_with_local_registry_and_accessories.yml
│ ├── deploy_with_local_registry_and_remote_builder.yml
│ ├── deploy_with_local_registry_and_remote_builder_with_port.yml
│ ├── deploy_with_multiple_proxy_roles.yml
│ ├── deploy_with_only_workers.yml
│ ├── deploy_with_parallel_roles.yml
│ ├── deploy_with_proxy.yml
│ ├── deploy_with_proxy_roles.yml
│ ├── deploy_with_proxy_run_config.yml
│ ├── deploy_with_proxy_run_config_conflicts.yml
│ ├── deploy_with_remote_builder.yml
│ ├── deploy_with_remote_builder_and_custom_ports.yml
│ ├── deploy_with_roles.yml
│ ├── deploy_with_roles_workers_primary.yml
│ ├── deploy_with_secrets.yml
│ ├── deploy_with_single_accessory.yml
│ ├── deploy_with_two_roles_one_host.yml
│ ├── deploy_with_uncommon_hostnames.yml
│ ├── deploy_without_clone.yml
│ ├── deploy_without_parallel_roles.yml
│ └── files/
│ ├── my.cnf
│ └── structure.sql.erb
├── git_test.rb
├── integration/
│ ├── accessory_test.rb
│ ├── app_test.rb
│ ├── broken_deploy_test.rb
│ ├── docker/
│ │ ├── deployer/
│ │ │ ├── .dockerignore
│ │ │ ├── Dockerfile
│ │ │ ├── app/
│ │ │ │ ├── .kamal/
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── docker-setup
│ │ │ │ │ │ ├── post-app-boot
│ │ │ │ │ │ ├── post-deploy
│ │ │ │ │ │ ├── post-proxy-reboot
│ │ │ │ │ │ ├── pre-app-boot
│ │ │ │ │ │ ├── pre-build
│ │ │ │ │ │ ├── pre-connect
│ │ │ │ │ │ ├── pre-deploy
│ │ │ │ │ │ └── pre-proxy-reboot
│ │ │ │ │ ├── secrets
│ │ │ │ │ └── secrets-common
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ ├── busybox.conf
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_custom_certificate/
│ │ │ │ ├── .kamal/
│ │ │ │ │ └── secrets
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── certs/
│ │ │ │ │ ├── cert.pem
│ │ │ │ │ └── key.pem
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_destinations/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ ├── deploy.production.yml
│ │ │ │ │ ├── deploy.staging.yml
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_parallel_roles/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ ├── default.conf
│ │ │ │ └── error_pages/
│ │ │ │ └── 503.html
│ │ │ ├── app_with_proxied_accessory/
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── app_with_roles/
│ │ │ │ ├── .kamal/
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── docker-setup
│ │ │ │ │ │ ├── post-deploy
│ │ │ │ │ │ ├── post-proxy-reboot
│ │ │ │ │ │ ├── pre-build
│ │ │ │ │ │ ├── pre-connect
│ │ │ │ │ │ ├── pre-deploy
│ │ │ │ │ │ └── pre-proxy-reboot
│ │ │ │ │ └── secrets
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ ├── default.conf
│ │ │ │ └── error_pages/
│ │ │ │ └── 503.html
│ │ │ ├── app_with_traefik/
│ │ │ │ ├── .kamal/
│ │ │ │ │ └── secrets
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── config/
│ │ │ │ │ └── deploy.yml
│ │ │ │ └── default.conf
│ │ │ ├── boot.sh
│ │ │ ├── break_app.sh
│ │ │ ├── setup.sh
│ │ │ └── update_app_rev.sh
│ │ ├── load_balancer/
│ │ │ ├── Dockerfile
│ │ │ └── default.conf
│ │ ├── registry/
│ │ │ ├── Dockerfile
│ │ │ └── boot.sh
│ │ ├── shared/
│ │ │ ├── .dockerignore
│ │ │ ├── Dockerfile
│ │ │ ├── boot.sh
│ │ │ └── registry-dns.conf
│ │ └── vm/
│ │ ├── Dockerfile
│ │ └── boot.sh
│ ├── docker-compose.yml
│ ├── integration_test.rb
│ ├── lock_test.rb
│ ├── main_test.rb
│ └── proxy_test.rb
├── secrets/
│ ├── aws_secrets_manager_adapter_test.rb
│ ├── bitwarden_adapter_test.rb
│ ├── bitwarden_secrets_manager_adapter_test.rb
│ ├── doppler_adapter_test.rb
│ ├── dotenv_inline_command_substitution_test.rb
│ ├── enpass_adapter_test.rb
│ ├── gcp_secret_manager_adapter_test.rb
│ ├── last_pass_adapter_test.rb
│ ├── one_password_adapter_test.rb
│ └── passbolt_adapter_test.rb
├── secrets_test.rb
├── sshkit_dns_retry_test.rb
├── test_helper.rb
└── utils_test.rb
SYMBOL INDEX (1275 symbols across 162 files)
FILE: lib/kamal.rb
type Kamal (line 1) | module Kamal
class ConfigurationError (line 2) | class ConfigurationError < StandardError; end
FILE: lib/kamal/cli.rb
type Kamal::Cli (line 1) | module Kamal::Cli
class BootError (line 2) | class BootError < StandardError; end
class HookError (line 3) | class HookError < StandardError; end
class LockError (line 4) | class LockError < StandardError; end
class DependencyError (line 5) | class DependencyError < StandardError; end
FILE: lib/kamal/cli/accessory.rb
class Kamal::Cli::Accessory (line 4) | class Kamal::Cli::Accessory < Kamal::Cli::Base
method boot (line 6) | def boot(name, prepare: true)
method upload (line 44) | def upload(name)
method directories (line 63) | def directories(name)
method reboot (line 78) | def reboot(name)
method start (line 93) | def start(name)
method stop (line 109) | def stop(name)
method restart (line 126) | def restart(name)
method details (line 134) | def details(name)
method exec (line 149) | def exec(name, *cmd)
method logs (line 191) | def logs(name)
method pull_image (line 215) | def pull_image(name)
method remove (line 228) | def remove(name)
method remove_container (line 241) | def remove_container(name)
method remove_image (line 253) | def remove_image(name)
method remove_service_directory (line 265) | def remove_service_directory(name)
method upgrade (line 278) | def upgrade(name)
method with_accessory (line 295) | def with_accessory(name)
method error_on_missing_accessory (line 304) | def error_on_missing_accessory(name)
method accessory_hosts (line 312) | def accessory_hosts(accessory)
method remove_accessory (line 316) | def remove_accessory(name)
method prepare (line 323) | def prepare(name)
FILE: lib/kamal/cli/alias/command.rb
class Kamal::Cli::Alias::Command (line 1) | class Kamal::Cli::Alias::Command < Thor::DynamicCommand
method run (line 2) | def run(instance, args = [])
FILE: lib/kamal/cli/app.rb
class Kamal::Cli::App (line 1) | class Kamal::Cli::App < Kamal::Cli::Base
method boot (line 3) | def boot
method start (line 44) | def start
method stop (line 63) | def stop
method details (line 84) | def details
method exec (line 96) | def exec(*cmd)
method containers (line 155) | def containers
method stale_containers (line 162) | def stale_containers
method images (line 185) | def images
method logs (line 198) | def logs
method remove (line 235) | def remove
method live (line 245) | def live
method maintenance (line 256) | def maintenance
method remove_container (line 267) | def remove_container(version)
method remove_containers (line 277) | def remove_containers
method remove_images (line 287) | def remove_images
method remove_app_directories (line 297) | def remove_app_directories
method version (line 308) | def version
method hosts_removing_all_roles (line 317) | def hosts_removing_all_roles
method using_version (line 321) | def using_version(new_version)
method current_running_version (line 335) | def current_running_version(host: KAMAL.primary_host)
method version_or_latest (line 344) | def version_or_latest
method with_lock_if_stopping (line 348) | def with_lock_if_stopping
method host_boot_groups (line 356) | def host_boot_groups
FILE: lib/kamal/cli/app/assets.rb
class Kamal::Cli::App::Assets (line 1) | class Kamal::Cli::App::Assets
method initialize (line 6) | def initialize(host, role, sshkit)
method run (line 12) | def run
method app (line 21) | def app
FILE: lib/kamal/cli/app/boot.rb
class Kamal::Cli::App::Boot (line 1) | class Kamal::Cli::App::Boot
method initialize (line 6) | def initialize(host, role, sshkit, version, barrier)
method run (line 14) | def run
method old_version_renamed_if_clashing (line 35) | def old_version_renamed_if_clashing
method start_new_version (line 46) | def start_new_version
method stop_new_version (line 66) | def stop_new_version
method stop_old_version (line 70) | def stop_old_version(version)
method release_barrier (line 76) | def release_barrier
method wait_at_barrier (line 82) | def wait_at_barrier
method close_barrier (line 91) | def close_barrier
method barrier_role? (line 103) | def barrier_role?
method app (line 107) | def app
method auditor (line 111) | def auditor
method audit (line 115) | def audit(message)
method gatekeeper? (line 119) | def gatekeeper?
method queuer? (line 123) | def queuer?
FILE: lib/kamal/cli/app/error_pages.rb
class Kamal::Cli::App::ErrorPages (line 1) | class Kamal::Cli::App::ErrorPages
method initialize (line 7) | def initialize(host, sshkit)
method run (line 12) | def run
method with_error_pages_tmpdir (line 22) | def with_error_pages_tmpdir
FILE: lib/kamal/cli/app/ssl_certificates.rb
class Kamal::Cli::App::SslCertificates (line 1) | class Kamal::Cli::App::SslCertificates
method initialize (line 5) | def initialize(host, role, sshkit)
method run (line 11) | def run
method app (line 25) | def app
FILE: lib/kamal/cli/base.rb
type Kamal::Cli (line 4) | module Kamal::Cli
class Base (line 5) | class Base < Thor
method exit_on_failure? (line 10) | def self.exit_on_failure?() true end
method dynamic_command_class (line 11) | def self.dynamic_command_class() Kamal::Cli::Alias::Command end
method initialize (line 27) | def initialize(args = [], local_options = {}, config = {})
method options_with_subcommand_class_options (line 40) | def options_with_subcommand_class_options
method initialize_commander (line 44) | def initialize_commander
method print_runtime (line 66) | def print_runtime
method with_lock (line 75) | def with_lock
method confirming (line 96) | def confirming(question)
method acquire_lock (line 106) | def acquire_lock
method release_lock (line 117) | def release_lock
method raise_if_locked (line 124) | def raise_if_locked
method run_hook (line 136) | def run_hook(hook, **extra_details)
method on (line 168) | def on(*args, &block)
method pre_connect_if_required (line 174) | def pre_connect_if_required
method command (line 181) | def command
method subcommand (line 192) | def subcommand
method first_invocation (line 199) | def first_invocation
method reset_invocation (line 203) | def reset_invocation(cli_class)
method ensure_run_directory (line 207) | def ensure_run_directory
method with_env (line 213) | def with_env(env)
method ensure_docker_installed (line 222) | def ensure_docker_installed
FILE: lib/kamal/cli/build.rb
class Kamal::Cli::Build (line 1) | class Kamal::Cli::Build < Kamal::Cli::Base
class BuildError (line 2) | class BuildError < StandardError; end
method deliver (line 5) | def deliver
method push (line 13) | def push
method pull (line 70) | def pull
method create (line 87) | def create
method remove (line 108) | def remove
method details (line 116) | def details
method dev (line 126) | def dev
method connect_to_remote_host (line 160) | def connect_to_remote_host(remote_host)
method mirror_hosts (line 173) | def mirror_hosts
method pull_on_hosts (line 188) | def pull_on_hosts(hosts)
method login_to_registry_locally (line 197) | def login_to_registry_locally
method login_to_registry_remotely (line 207) | def login_to_registry_remotely
method forward_local_registry_port_for_remote_builder (line 213) | def forward_local_registry_port_for_remote_builder(&block)
method forward_local_registry_port (line 222) | def forward_local_registry_port(hosts, **ssh_options, &block)
method remote_builder_ssh_options (line 231) | def remote_builder_ssh_options(remote_uri)
FILE: lib/kamal/cli/build/clone.rb
class Kamal::Cli::Build::Clone (line 1) | class Kamal::Cli::Build::Clone
method initialize (line 5) | def initialize(sshkit)
method prepare (line 9) | def prepare
method clone_repo (line 30) | def clone_repo
method reset (line 37) | def reset
method validate! (line 45) | def validate!
FILE: lib/kamal/cli/build/port_forwarding.rb
class Kamal::Cli::Build::PortForwarding (line 3) | class Kamal::Cli::Build::PortForwarding
method initialize (line 6) | def initialize(hosts, port, **ssh_options)
method forward (line 12) | def forward
method stop (line 22) | def stop
method forward_ports (line 27) | def forward_ports
method error (line 63) | def error(message)
FILE: lib/kamal/cli/healthcheck/barrier.rb
class Kamal::Cli::Healthcheck::Barrier (line 3) | class Kamal::Cli::Healthcheck::Barrier
method initialize (line 4) | def initialize
method close (line 8) | def close
method open (line 12) | def open
method wait (line 16) | def wait
method opened? (line 23) | def opened?
method set (line 27) | def set(value)
FILE: lib/kamal/cli/healthcheck/error.rb
class Kamal::Cli::Healthcheck::Error (line 1) | class Kamal::Cli::Healthcheck::Error < StandardError
FILE: lib/kamal/cli/healthcheck/poller.rb
type Kamal::Cli::Healthcheck::Poller (line 1) | module Kamal::Cli::Healthcheck::Poller
function wait_for_healthy (line 4) | def wait_for_healthy(&block)
function info (line 39) | def info(message)
FILE: lib/kamal/cli/lock.rb
class Kamal::Cli::Lock (line 1) | class Kamal::Cli::Lock < Kamal::Cli::Base
method status (line 3) | def status
method acquire (line 13) | def acquire
method release (line 26) | def release
method handle_missing_lock (line 36) | def handle_missing_lock
FILE: lib/kamal/cli/main.rb
class Kamal::Cli::Main (line 1) | class Kamal::Cli::Main < Kamal::Cli::Base
method setup (line 5) | def setup
method deploy (line 21) | def deploy(boot_accessories: false)
method redeploy (line 57) | def redeploy
method rollback (line 83) | def rollback(version)
method details (line 107) | def details
method audit (line 114) | def audit
method config (line 122) | def config
method docs (line 129) | def docs(section = nil)
method init (line 142) | def init
method remove (line 183) | def remove
method upgrade (line 197) | def upgrade
method version (line 226) | def version
method container_available? (line 258) | def container_available?(version)
method deploy_options (line 278) | def deploy_options
FILE: lib/kamal/cli/proxy.rb
class Kamal::Cli::Proxy (line 1) | class Kamal::Cli::Proxy < Kamal::Cli::Base
method boot (line 3) | def boot
method boot_config (line 37) | def boot_config(subcommand)
method reboot (line 109) | def reboot
method upgrade (line 137) | def upgrade
method start (line 176) | def start
method stop (line 186) | def stop
method restart (line 196) | def restart
method details (line 204) | def details
method logs (line 215) | def logs
method remove (line 238) | def remove
method remove_container (line 250) | def remove_container
method remove_image (line 260) | def remove_image
method remove_proxy_directory (line 270) | def remove_proxy_directory
method removal_allowed? (line 279) | def removal_allowed?(force)
FILE: lib/kamal/cli/prune.rb
class Kamal::Cli::Prune (line 1) | class Kamal::Cli::Prune < Kamal::Cli::Base
method all (line 3) | def all
method images (line 11) | def images
method containers (line 23) | def containers
FILE: lib/kamal/cli/registry.rb
class Kamal::Cli::Registry (line 1) | class Kamal::Cli::Registry < Kamal::Cli::Base
method setup (line 5) | def setup
method remove (line 19) | def remove
method login (line 31) | def login
method logout (line 42) | def logout
FILE: lib/kamal/cli/secrets.rb
class Kamal::Cli::Secrets (line 1) | class Kamal::Cli::Secrets < Kamal::Cli::Base
method fetch (line 7) | def fetch(*secrets)
method extract (line 22) | def extract(name, secrets)
method print (line 32) | def print
method initialize_adapter (line 39) | def initialize_adapter(adapter)
method return_or_puts (line 43) | def return_or_puts(value, inline: nil)
FILE: lib/kamal/cli/server.rb
class Kamal::Cli::Server (line 1) | class Kamal::Cli::Server < Kamal::Cli::Base
method exec (line 4) | def exec(*cmd)
method bootstrap (line 29) | def bootstrap
FILE: lib/kamal/commander.rb
class Kamal::Commander (line 5) | class Kamal::Commander
method initialize (line 10) | def initialize
method reset (line 14) | def reset
method config (line 23) | def config
method configure (line 30) | def configure(**kwargs)
method configured? (line 34) | def configured?
method specific_primary! (line 38) | def specific_primary!
method specific_roles= (line 47) | def specific_roles=(role_names)
method specific_hosts= (line 56) | def specific_hosts=(hosts)
method with_specific_hosts (line 65) | def with_specific_hosts(hosts)
method accessory_names (line 72) | def accessory_names
method app (line 76) | def app(role: nil, host: nil)
method accessory (line 80) | def accessory(name)
method auditor (line 84) | def auditor(**details)
method builder (line 88) | def builder
method docker (line 92) | def docker
method hook (line 96) | def hook
method lock (line 100) | def lock
method proxy (line 104) | def proxy(host)
method prune (line 108) | def prune
method registry (line 112) | def registry
method server (line 116) | def server
method alias (line 120) | def alias(name)
method resolve_alias (line 124) | def resolve_alias(name)
method with_verbosity (line 133) | def with_verbosity(level)
method holding_lock? (line 145) | def holding_lock?
method connected? (line 149) | def connected?
method configure_sshkit_with (line 155) | def configure_sshkit_with(config)
method specifics (line 166) | def specifics
FILE: lib/kamal/commander/specifics.rb
class Kamal::Commander::Specifics (line 1) | class Kamal::Commander::Specifics
method initialize (line 5) | def initialize(config, specific_hosts, specific_roles)
method roles_on (line 17) | def roles_on(host)
method app_hosts (line 21) | def app_hosts
method proxy_hosts (line 25) | def proxy_hosts
method accessory_hosts (line 29) | def accessory_hosts
method primary_specific_role (line 36) | def primary_specific_role
method primary_or_first_role (line 40) | def primary_or_first_role(roles)
method specified_roles (line 44) | def specified_roles
method specified_hosts (line 49) | def specified_hosts
method sort_primary_role_hosts_first! (line 59) | def sort_primary_role_hosts_first!(hosts)
FILE: lib/kamal/commands.rb
type Kamal::Commands (line 1) | module Kamal::Commands
FILE: lib/kamal/commands/accessory.rb
class Kamal::Commands::Accessory (line 1) | class Kamal::Commands::Accessory < Kamal::Commands::Base
method initialize (line 10) | def initialize(config, name:)
method run (line 15) | def run(host: nil)
method start (line 32) | def start
method stop (line 36) | def stop
method info (line 40) | def info(all: false, quiet: false)
method logs (line 44) | def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_opt...
method follow_logs (line 50) | def follow_logs(timestamps: true, grep: nil, grep_options: nil)
method execute_in_existing_container (line 57) | def execute_in_existing_container(*command, interactive: false)
method execute_in_new_container (line 64) | def execute_in_new_container(*command, interactive: false)
method execute_in_existing_container_over_ssh (line 76) | def execute_in_existing_container_over_ssh(*command)
method execute_in_new_container_over_ssh (line 80) | def execute_in_new_container_over_ssh(*command)
method run_over_ssh (line 84) | def run_over_ssh(command)
method ensure_local_file_present (line 88) | def ensure_local_file_present(local_file)
method pull_image (line 94) | def pull_image
method remove_service_directory (line 98) | def remove_service_directory
method remove_container (line 102) | def remove_container
method remove_image (line 106) | def remove_image
method ensure_env_directory (line 110) | def ensure_env_directory
method service_filter (line 115) | def service_filter
FILE: lib/kamal/commands/accessory/proxy.rb
type Kamal::Commands::Accessory::Proxy (line 1) | module Kamal::Commands::Accessory::Proxy
function deploy (line 4) | def deploy(target:)
function remove (line 8) | def remove
function proxy_exec (line 13) | def proxy_exec(*command)
FILE: lib/kamal/commands/app.rb
class Kamal::Commands::App (line 1) | class Kamal::Commands::App < Kamal::Commands::Base
method initialize (line 10) | def initialize(config, role: nil, host: nil)
method run (line 16) | def run(hostname: nil)
method start (line 37) | def start
method status (line 41) | def status(version:)
method stop (line 45) | def stop(version: nil)
method info (line 51) | def info
method current_running_container_id (line 56) | def current_running_container_id
method container_id_for_version (line 60) | def container_id_for_version(version, only_running: false)
method current_running_version (line 64) | def current_running_version
method list_versions (line 70) | def list_versions(*docker_args, statuses: nil)
method ensure_env_directory (line 76) | def ensure_env_directory
method latest_image_id (line 81) | def latest_image_id
method current_running_container (line 85) | def current_running_container(format:)
method latest_image_container (line 91) | def latest_image_container(format:)
method latest_container (line 95) | def latest_container(format:, filters: nil)
method container_filter_args (line 99) | def container_filter_args(statuses: nil)
method image_filter_args (line 103) | def image_filter_args
method extract_version_from_name (line 107) | def extract_version_from_name
method container_filters (line 112) | def container_filters(statuses: nil)
method image_filters (line 122) | def image_filters
FILE: lib/kamal/commands/app/assets.rb
type Kamal::Commands::App::Assets (line 1) | module Kamal::Commands::App::Assets
function extract_assets (line 2) | def extract_assets
function sync_asset_volumes (line 14) | def sync_asset_volumes(old_version: nil)
function clean_up_assets (line 30) | def clean_up_assets
function find_and_remove_older_siblings (line 37) | def find_and_remove_older_siblings(path)
function copy_contents (line 48) | def copy_contents(source, destination, continue_on_error: false)
FILE: lib/kamal/commands/app/containers.rb
type Kamal::Commands::App::Containers (line 1) | module Kamal::Commands::App::Containers
function list_containers (line 4) | def list_containers
function list_container_names (line 8) | def list_container_names
function remove_container (line 12) | def remove_container(version:)
function rename_container (line 18) | def rename_container(version:, new_version:)
function remove_containers (line 22) | def remove_containers
function container_health_log (line 26) | def container_health_log(version:)
FILE: lib/kamal/commands/app/error_pages.rb
type Kamal::Commands::App::ErrorPages (line 1) | module Kamal::Commands::App::ErrorPages
function create_error_pages_directory (line 2) | def create_error_pages_directory
function clean_up_error_pages (line 6) | def clean_up_error_pages
FILE: lib/kamal/commands/app/execution.rb
type Kamal::Commands::App::Execution (line 1) | module Kamal::Commands::App::Execution
function execute_in_existing_container (line 2) | def execute_in_existing_container(*command, interactive: false, env:)
function execute_in_new_container (line 10) | def execute_in_new_container(*command, interactive: false, detach: fal...
function execute_in_existing_container_over_ssh (line 26) | def execute_in_existing_container_over_ssh(*command, env:)
function execute_in_new_container_over_ssh (line 30) | def execute_in_new_container_over_ssh(*command, env:)
function container_name_for_exec (line 35) | def container_name_for_exec
FILE: lib/kamal/commands/app/images.rb
type Kamal::Commands::App::Images (line 1) | module Kamal::Commands::App::Images
function list_images (line 2) | def list_images
function remove_images (line 6) | def remove_images
function tag_latest_image (line 10) | def tag_latest_image
FILE: lib/kamal/commands/app/logging.rb
type Kamal::Commands::App::Logging (line 1) | module Kamal::Commands::App::Logging
function logs (line 2) | def logs(container_id: nil, timestamps: true, since: nil, lines: nil, ...
function follow_logs (line 9) | def follow_logs(host:, container_id: nil, timestamps: true, lines: nil...
function container_id_command (line 21) | def container_id_command(container_id)
FILE: lib/kamal/commands/app/proxy.rb
type Kamal::Commands::App::Proxy (line 1) | module Kamal::Commands::App::Proxy
function deploy (line 4) | def deploy(target:)
function remove (line 8) | def remove
function live (line 12) | def live
function maintenance (line 16) | def maintenance(**options)
function remove_proxy_app_directory (line 20) | def remove_proxy_app_directory
function create_ssl_directory (line 24) | def create_ssl_directory
function proxy_exec (line 29) | def proxy_exec(*command)
FILE: lib/kamal/commands/auditor.rb
class Kamal::Commands::Auditor (line 1) | class Kamal::Commands::Auditor < Kamal::Commands::Base
method initialize (line 5) | def initialize(config, **details)
method record (line 11) | def record(line, **details)
method reveal (line 17) | def reveal
method audit_log_file (line 22) | def audit_log_file
method audit_tags (line 28) | def audit_tags(**details)
method make_run_directory (line 32) | def make_run_directory
method audit_line (line 36) | def audit_line(line, **details)
FILE: lib/kamal/commands/base.rb
type Kamal::Commands (line 1) | module Kamal::Commands
class Base (line 2) | class Base
method initialize (line 9) | def initialize(config)
method run_over_ssh (line 13) | def run_over_ssh(*command, host:)
method container_id_for (line 17) | def container_id_for(container_name:, only_running: false)
method make_directory_for (line 21) | def make_directory_for(remote_file)
method make_directory (line 25) | def make_directory(path)
method remove_directory (line 29) | def remove_directory(path)
method remove_file (line 33) | def remove_file(path)
method ensure_docker_installed (line 37) | def ensure_docker_installed
method combine (line 44) | def combine(*commands, by: "&&")
method chain (line 51) | def chain(*commands)
method pipe (line 55) | def pipe(*commands)
method append (line 59) | def append(*commands)
method write (line 63) | def write(*commands)
method any (line 67) | def any(*commands)
method substitute (line 71) | def substitute(*commands)
method xargs (line 75) | def xargs(command)
method shell (line 79) | def shell(command)
method docker (line 83) | def docker(*args)
method pack (line 87) | def pack(*args)
method git (line 91) | def git(*args, path: nil)
method grep (line 95) | def grep(*args)
method tags (line 99) | def tags(**details)
method ssh_config_args (line 103) | def ssh_config_args
method ssh_proxy_args (line 116) | def ssh_proxy_args
method ssh_keys_args (line 125) | def ssh_keys_args
method ssh_keys (line 129) | def ssh_keys
method ensure_local_docker_installed (line 135) | def ensure_local_docker_installed
method ensure_local_buildx_installed (line 139) | def ensure_local_buildx_installed
method docker_interactive_args (line 143) | def docker_interactive_args
FILE: lib/kamal/commands/builder.rb
class Kamal::Commands::Builder (line 3) | class Kamal::Commands::Builder < Kamal::Commands::Base
method name (line 15) | def name
method target (line 19) | def target
method remote (line 35) | def remote
method local (line 39) | def local
method hybrid (line 43) | def hybrid
method pack (line 47) | def pack
method cloud (line 51) | def cloud
FILE: lib/kamal/commands/builder/base.rb
class Kamal::Commands::Builder::Base (line 1) | class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError (line 2) | class BuilderError < StandardError; end
method clean (line 13) | def clean
method push (line 17) | def push(export_action = "registry", tag_as_dirty: false, no_cache: fa...
method pull (line 29) | def pull
method info (line 33) | def info
method inspect_builder (line 39) | def inspect_builder
method build_options (line 43) | def build_options
method build_context (line 47) | def build_context
method validate_image (line 51) | def validate_image
method first_mirror (line 60) | def first_mirror
method login_to_registry_locally? (line 64) | def login_to_registry_locally?
method push_env (line 68) | def push_env
method build_tag_names (line 73) | def build_tag_names(tag_as_dirty: false)
method build_tag_options (line 79) | def build_tag_options(tag_as_dirty: false)
method build_cache (line 83) | def build_cache
method build_labels (line 90) | def build_labels
method build_args (line 94) | def build_args
method build_secrets (line 98) | def build_secrets
method build_dockerfile (line 102) | def build_dockerfile
method build_target (line 110) | def build_target
method build_ssh (line 114) | def build_ssh
method builder_provenance (line 118) | def builder_provenance
method builder_sbom (line 122) | def builder_sbom
method builder_config (line 126) | def builder_config
method registry_config (line 130) | def registry_config
method driver_options (line 134) | def driver_options
method platform_options (line 140) | def platform_options(arches)
FILE: lib/kamal/commands/builder/clone.rb
type Kamal::Commands::Builder::Clone (line 1) | module Kamal::Commands::Builder::Clone
function clone (line 2) | def clone
function clone_reset_steps (line 6) | def clone_reset_steps
function clone_status (line 16) | def clone_status
function clone_revision (line 20) | def clone_revision
function escaped_root (line 24) | def escaped_root
function escaped_build_directory (line 28) | def escaped_build_directory
FILE: lib/kamal/commands/builder/cloud.rb
class Kamal::Commands::Builder::Cloud (line 1) | class Kamal::Commands::Builder::Cloud < Kamal::Commands::Builder::Base
method create (line 4) | def create
method remove (line 8) | def remove
method builder_name (line 13) | def builder_name
method inspect_buildx (line 17) | def inspect_buildx
FILE: lib/kamal/commands/builder/hybrid.rb
class Kamal::Commands::Builder::Hybrid (line 1) | class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
method create (line 2) | def create
method builder_name (line 10) | def builder_name
method create_local_buildx (line 14) | def create_local_buildx
method append_remote_buildx (line 18) | def append_remote_buildx
FILE: lib/kamal/commands/builder/local.rb
class Kamal::Commands::Builder::Local (line 1) | class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
method create (line 2) | def create
method remove (line 8) | def remove
method builder_name (line 13) | def builder_name
FILE: lib/kamal/commands/builder/pack.rb
class Kamal::Commands::Builder::Pack (line 1) | class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
method push (line 2) | def push(export_action = "registry", tag_as_dirty: false, no_cache: fa...
method remove (line 8) | def remove;end
method info (line 10) | def info
method build (line 16) | def build(tag_as_dirty: false, no_cache: false)
method export (line 31) | def export(export_action)
method platform (line 39) | def platform
method buildpacks (line 43) | def buildpacks
FILE: lib/kamal/commands/builder/remote.rb
class Kamal::Commands::Builder::Remote (line 1) | class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
method create (line 2) | def create
method remove (line 8) | def remove
method info (line 14) | def info
method inspect_builder (line 20) | def inspect_builder
method login_to_registry_locally? (line 27) | def login_to_registry_locally?
method push_env (line 31) | def push_env
method builder_name (line 36) | def builder_name
method remote_context_name (line 40) | def remote_context_name
method remote_builder_name_suffix (line 44) | def remote_builder_name_suffix
method inspect_buildx (line 48) | def inspect_buildx
method inspect_remote_context (line 54) | def inspect_remote_context
method create_remote_context (line 60) | def create_remote_context
method remove_remote_context (line 64) | def remove_remote_context
method create_buildx (line 68) | def create_buildx
method remove_buildx (line 72) | def remove_buildx
FILE: lib/kamal/commands/docker.rb
class Kamal::Commands::Docker (line 1) | class Kamal::Commands::Docker < Kamal::Commands::Base
method install (line 3) | def install
method installed? (line 8) | def installed?
method running? (line 13) | def running?
method superuser? (line 18) | def superuser?
method root? (line 22) | def root?
method in_docker_group? (line 26) | def in_docker_group?
method add_to_docker_group (line 30) | def add_to_docker_group
method refresh_session (line 34) | def refresh_session
method create_network (line 38) | def create_network
method get_docker (line 43) | def get_docker
FILE: lib/kamal/commands/hook.rb
class Kamal::Commands::Hook (line 1) | class Kamal::Commands::Hook < Kamal::Commands::Base
method run (line 2) | def run(hook)
method env (line 6) | def env(secrets: false, **details)
method hook_exists? (line 12) | def hook_exists?(hook)
method hook_file (line 17) | def hook_file(hook)
FILE: lib/kamal/commands/lock.rb
class Kamal::Commands::Lock (line 5) | class Kamal::Commands::Lock < Kamal::Commands::Base
method acquire (line 6) | def acquire(message, version)
method release (line 12) | def release
method status (line 18) | def status
method ensure_locks_directory (line 24) | def ensure_locks_directory
method write_lock_details (line 29) | def write_lock_details(message, version)
method read_lock_details (line 35) | def read_lock_details
method stat_lock_dir (line 41) | def stat_lock_dir
method lock_dir (line 47) | def lock_dir
method lock_details_file (line 53) | def lock_details_file
method lock_details (line 57) | def lock_details(message, version)
method locked_by (line 65) | def locked_by
FILE: lib/kamal/commands/proxy.rb
class Kamal::Commands::Proxy (line 1) | class Kamal::Commands::Proxy < Kamal::Commands::Base
method initialize (line 5) | def initialize(config, host:)
method run (line 10) | def run
method start (line 27) | def start
method stop (line 31) | def stop(name: container_name)
method start_or_run (line 35) | def start_or_run
method info (line 39) | def info
method version (line 43) | def version
method logs (line 49) | def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_opt...
method follow_logs (line 55) | def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
method remove_container (line 62) | def remove_container
method remove_image (line 66) | def remove_image
method cleanup_traefik (line 70) | def cleanup_traefik
method ensure_proxy_directory (line 79) | def ensure_proxy_directory
method remove_proxy_directory (line 83) | def remove_proxy_directory
method ensure_apps_config_directory (line 87) | def ensure_apps_config_directory
method boot_config (line 91) | def boot_config
method read_boot_options (line 95) | def read_boot_options
method read_image (line 99) | def read_image
method read_image_version (line 103) | def read_image_version
method read_run_command (line 107) | def read_run_command
method reset_boot_options (line 111) | def reset_boot_options
method reset_image (line 115) | def reset_image
method reset_image_version (line 119) | def reset_image_version
method reset_run_command (line 123) | def reset_run_command
method container_name (line 128) | def container_name
method read_file (line 132) | def read_file(file, default: nil)
method docker_run (line 136) | def docker_run
FILE: lib/kamal/commands/prune.rb
class Kamal::Commands::Prune (line 4) | class Kamal::Commands::Prune < Kamal::Commands::Base
method dangling_images (line 5) | def dangling_images
method tagged_images (line 9) | def tagged_images
method app_containers (line 16) | def app_containers(retain:)
method stopped_containers_filters (line 24) | def stopped_containers_filters
method active_image_list (line 28) | def active_image_list
method service_filter (line 35) | def service_filter
FILE: lib/kamal/commands/registry.rb
class Kamal::Commands::Registry (line 1) | class Kamal::Commands::Registry < Kamal::Commands::Base
method login (line 2) | def login(registry_config: nil)
method logout (line 13) | def logout(registry_config: nil)
method setup (line 19) | def setup(registry_config: nil)
method remove (line 28) | def remove
method local? (line 35) | def local?
FILE: lib/kamal/commands/server.rb
class Kamal::Commands::Server (line 1) | class Kamal::Commands::Server < Kamal::Commands::Base
method ensure_run_directory (line 2) | def ensure_run_directory
method remove_app_directory (line 6) | def remove_app_directory
method app_directory_count (line 10) | def app_directory_count
FILE: lib/kamal/configuration.rb
class Kamal::Configuration (line 8) | class Kamal::Configuration
method create_from (line 20) | def create_from(config_file:, destination: nil, version: nil)
method load_raw_config (line 28) | def load_raw_config(config_file:, destination: nil)
method load_config_files (line 33) | def load_config_files(*files)
method load_config_file (line 37) | def load_config_file(file)
method destination_config_file (line 49) | def destination_config_file(base_config_file, destination)
method initialize (line 54) | def initialize(raw_config, destination: nil, version: nil, validate: t...
method version= (line 92) | def version=(version)
method version (line 96) | def version
method abbreviated_version (line 100) | def abbreviated_version
method minimum_version (line 111) | def minimum_version
method service_and_destination (line 115) | def service_and_destination
method roles (line 119) | def roles
method role (line 123) | def role(name)
method accessory (line 127) | def accessory(name)
method all_hosts (line 131) | def all_hosts
method host_roles (line 135) | def host_roles(host)
method host_accessories (line 139) | def host_accessories(host)
method app_hosts (line 143) | def app_hosts
method primary_host (line 147) | def primary_host
method primary_role_name (line 151) | def primary_role_name
method primary_role (line 155) | def primary_role
method allow_empty_roles? (line 159) | def allow_empty_roles?
method proxy_roles (line 163) | def proxy_roles
method proxy_role_names (line 167) | def proxy_role_names
method proxy_accessories (line 171) | def proxy_accessories
method proxy_hosts (line 175) | def proxy_hosts
method image (line 179) | def image
method proxy_run (line 186) | def proxy_run(host)
method repository (line 191) | def repository
method absolute_image (line 195) | def absolute_image
method latest_image (line 199) | def latest_image
method latest_tag (line 203) | def latest_tag
method service_with_version (line 207) | def service_with_version
method require_destination? (line 211) | def require_destination?
method retain_containers (line 215) | def retain_containers
method volume_args (line 219) | def volume_args
method logging_args (line 227) | def logging_args
method readiness_delay (line 231) | def readiness_delay
method deploy_timeout (line 235) | def deploy_timeout
method drain_timeout (line 239) | def drain_timeout
method run_directory (line 243) | def run_directory
method apps_directory (line 247) | def apps_directory
method app_directory (line 251) | def app_directory
method env_directory (line 255) | def env_directory
method assets_directory (line 259) | def assets_directory
method hooks_path (line 263) | def hooks_path
method secrets_path (line 267) | def secrets_path
method asset_path (line 271) | def asset_path
method error_pages_path (line 275) | def error_pages_path
method env_tags (line 279) | def env_tags
method env_tag (line 287) | def env_tag(name)
method hooks_output_for (line 291) | def hooks_output_for(hook)
method to_h (line 300) | def to_h
method ensure_destination_if_required (line 320) | def ensure_destination_if_required
method ensure_required_keys_present (line 328) | def ensure_required_keys_present
method ensure_valid_service_name (line 358) | def ensure_valid_service_name
method ensure_valid_kamal_version (line 364) | def ensure_valid_kamal_version
method ensure_retain_containers_valid (line 372) | def ensure_retain_containers_valid
method ensure_no_traefik_reboot_hooks (line 378) | def ensure_no_traefik_reboot_hooks
method ensure_one_host_for_ssl_roles (line 388) | def ensure_one_host_for_ssl_roles
method ensure_unique_hosts_for_ssl_roles (line 394) | def ensure_unique_hosts_for_ssl_roles
method ensure_local_registry_remote_builder_has_ssh_url (line 403) | def ensure_local_registry_remote_builder_has_ssh_url
method ensure_no_conflicting_proxy_runs (line 413) | def ensure_no_conflicting_proxy_runs
method proxy_runs (line 422) | def proxy_runs(host)
method role_names (line 426) | def role_names
method ensure_valid_hooks_output! (line 430) | def ensure_valid_hooks_output!
method validate_hooks_output_level! (line 439) | def validate_hooks_output_level!(level, hook = nil)
method git_version (line 445) | def git_version
FILE: lib/kamal/configuration/accessory.rb
class Kamal::Configuration::Accessory (line 1) | class Kamal::Configuration::Accessory
method initialize (line 10) | def initialize(name, config:)
method service_name (line 26) | def service_name
method image (line 30) | def image
method hosts (line 34) | def hosts
method port (line 38) | def port
method network_args (line 44) | def network_args
method publish_args (line 48) | def publish_args
method labels (line 52) | def labels
method label_args (line 56) | def label_args
method env_args (line 60) | def env_args
method env_directory (line 64) | def env_directory
method secrets_io (line 68) | def secrets_io
method secrets_path (line 72) | def secrets_path
method files (line 76) | def files
method directories (line 88) | def directories
method volume_args (line 100) | def volume_args
method option_args (line 104) | def option_args
method cmd (line 112) | def cmd
method running_proxy? (line 116) | def running_proxy?
method initialize_env (line 123) | def initialize_env
method initialize_proxy (line 130) | def initialize_proxy
method initialize_registry (line 138) | def initialize_registry
method default_labels (line 145) | def default_labels
method expand_local_file (line 149) | def expand_local_file(local_file)
method with_env_loaded (line 157) | def with_env_loaded
method read_dynamic_file (line 164) | def read_dynamic_file(local_file)
method expand_remote_file (line 168) | def expand_remote_file(remote_file)
method specific_volumes (line 172) | def specific_volumes
method path_volumes (line 176) | def path_volumes(paths)
method parse_path_config (line 185) | def parse_path_config(config, default_mode:)
method expand_host_path (line 211) | def expand_host_path(host_path)
method expand_host_path_for_volume (line 215) | def expand_host_path_for_volume(host_path)
method absolute_path? (line 219) | def absolute_path?(path)
method service_data_directory (line 223) | def service_data_directory
method hosts_from_host (line 227) | def hosts_from_host
method hosts_from_hosts (line 231) | def hosts_from_hosts
method hosts_from_roles (line 235) | def hosts_from_roles
method hosts_from_tags (line 243) | def hosts_from_tags
method extract_hosts_from_config_with_tag (line 251) | def extract_hosts_from_config_with_tag(tag)
method network (line 261) | def network
method ensure_valid_roles (line 265) | def ensure_valid_roles
FILE: lib/kamal/configuration/alias.rb
class Kamal::Configuration::Alias (line 1) | class Kamal::Configuration::Alias
method initialize (line 6) | def initialize(name, config:)
FILE: lib/kamal/configuration/boot.rb
class Kamal::Configuration::Boot (line 1) | class Kamal::Configuration::Boot
method initialize (line 6) | def initialize(config:)
method limit (line 12) | def limit
method wait (line 22) | def wait
method parallel_roles (line 26) | def parallel_roles
FILE: lib/kamal/configuration/builder.rb
class Kamal::Configuration::Builder (line 1) | class Kamal::Configuration::Builder
method initialize (line 8) | def initialize(config:)
method to_h (line 18) | def to_h
method remote (line 22) | def remote
method arches (line 26) | def arches
method local_arches (line 30) | def local_arches
method remote_arches (line 40) | def remote_arches
method remote? (line 48) | def remote?
method local? (line 52) | def local?
method cloud? (line 56) | def cloud?
method cached? (line 60) | def cached?
method pack? (line 64) | def pack?
method args (line 68) | def args
method secrets (line 72) | def secrets
method dockerfile (line 76) | def dockerfile
method target (line 80) | def target
method context (line 84) | def context
method driver (line 88) | def driver
method pack_builder (line 92) | def pack_builder
method pack_buildpacks (line 96) | def pack_buildpacks
method local_disabled? (line 100) | def local_disabled?
method cache_from (line 104) | def cache_from
method cache_to (line 115) | def cache_to
method ssh (line 126) | def ssh
method provenance (line 130) | def provenance
method sbom (line 134) | def sbom
method git_clone? (line 138) | def git_clone?
method clone_directory (line 142) | def clone_directory
method build_directory (line 146) | def build_directory
method docker_driver? (line 155) | def docker_driver?
method valid? (line 160) | def valid?
method cache_image (line 172) | def cache_image
method cache_image_ref (line 176) | def cache_image_ref
method cache_options (line 180) | def cache_options
method cache_from_config_for_gha (line 184) | def cache_from_config_for_gha
method cache_from_config_for_registry (line 191) | def cache_from_config_for_registry
method cache_to_config_for_gha (line 195) | def cache_to_config_for_gha
method cache_to_config_for_registry (line 199) | def cache_to_config_for_registry
method repo_basename (line 203) | def repo_basename
method repo_relative_pwd (line 207) | def repo_relative_pwd
method pwd_sha (line 211) | def pwd_sha
method default_arch (line 215) | def default_arch
FILE: lib/kamal/configuration/env.rb
class Kamal::Configuration::Env (line 1) | class Kamal::Configuration::Env
method initialize (line 7) | def initialize(config:, secrets:, context: "env")
method clear_args (line 15) | def clear_args
method secrets_io (line 19) | def secrets_io
method merge (line 23) | def merge(other)
method to_h (line 29) | def to_h
method aliased_secrets (line 34) | def aliased_secrets
method extract_alias (line 38) | def extract_alias(key)
FILE: lib/kamal/configuration/env/tag.rb
class Kamal::Configuration::Env::Tag (line 1) | class Kamal::Configuration::Env::Tag
method initialize (line 4) | def initialize(name, config:, secrets:)
method env (line 10) | def env
FILE: lib/kamal/configuration/logging.rb
class Kamal::Configuration::Logging (line 1) | class Kamal::Configuration::Logging
method initialize (line 8) | def initialize(logging_config:, context: "logging")
method driver (line 13) | def driver
method options (line 17) | def options
method merge (line 21) | def merge(other)
method args (line 25) | def args
FILE: lib/kamal/configuration/proxy.rb
class Kamal::Configuration::Proxy (line 1) | class Kamal::Configuration::Proxy
method initialize (line 10) | def initialize(config:, proxy_config:, role_name: nil, secrets:, conte...
method app_port (line 20) | def app_port
method ssl? (line 24) | def ssl?
method hosts (line 28) | def hosts
method custom_ssl_certificate? (line 32) | def custom_ssl_certificate?
method certificate_pem_content (line 38) | def certificate_pem_content
method private_key_pem_content (line 44) | def private_key_pem_content
method host_tls_cert (line 50) | def host_tls_cert
method host_tls_key (line 54) | def host_tls_key
method container_tls_cert (line 58) | def container_tls_cert
method container_tls_key (line 62) | def container_tls_key
method path_prefixes (line 66) | def path_prefixes
method deploy_options (line 70) | def deploy_options
method deploy_command_args (line 97) | def deploy_command_args(target:)
method stop_options (line 101) | def stop_options(drain_timeout: nil, message: nil)
method stop_command_args (line 108) | def stop_command_args(**options)
method merge (line 112) | def merge(other)
method tls_path (line 117) | def tls_path(directory, filename)
method seconds_duration (line 121) | def seconds_duration(value)
method error_pages (line 125) | def error_pages
FILE: lib/kamal/configuration/proxy/boot.rb
class Kamal::Configuration::Proxy::Boot (line 1) | class Kamal::Configuration::Proxy::Boot
method initialize (line 5) | def initialize(config:)
method publish_args (line 9) | def publish_args(http_port, https_port, bind_ips = nil)
method logging_args (line 21) | def logging_args(max_size)
method default_boot_options (line 25) | def default_boot_options
method repository_name (line 32) | def repository_name
method image_name (line 36) | def image_name
method image_default (line 40) | def image_default
method container_name (line 44) | def container_name
method host_directory (line 48) | def host_directory
method options_file (line 52) | def options_file
method image_file (line 56) | def image_file
method image_version_file (line 60) | def image_version_file
method run_command_file (line 64) | def run_command_file
method apps_directory (line 68) | def apps_directory
method apps_container_directory (line 72) | def apps_container_directory
method apps_volume (line 76) | def apps_volume
method app_directory (line 82) | def app_directory
method app_container_directory (line 86) | def app_container_directory
method error_pages_directory (line 90) | def error_pages_directory
method error_pages_container_directory (line 94) | def error_pages_container_directory
method tls_directory (line 98) | def tls_directory
method tls_container_directory (line 102) | def tls_container_directory
method ensure_valid_bind_ips (line 107) | def ensure_valid_bind_ips(bind_ips)
method format_bind_ip (line 116) | def format_bind_ip(ip)
FILE: lib/kamal/configuration/proxy/run.rb
class Kamal::Configuration::Proxy::Run (line 1) | class Kamal::Configuration::Proxy::Run
method initialize (line 10) | def initialize(config, run_config:, context: "proxy/run")
method debug? (line 16) | def debug?
method publish? (line 20) | def publish?
method http_port (line 24) | def http_port
method https_port (line 28) | def https_port
method bind_ips (line 32) | def bind_ips
method publish_args (line 36) | def publish_args
method log_max_size (line 48) | def log_max_size
method logging_args (line 52) | def logging_args
method version (line 56) | def version
method registry (line 60) | def registry
method repository (line 64) | def repository
method image (line 68) | def image
method container_name (line 72) | def container_name
method options_args (line 76) | def options_args
method run_command (line 82) | def run_command
method metrics_port (line 86) | def metrics_port
method run_command_options (line 90) | def run_command_options
method docker_options_args (line 94) | def docker_options_args
method host_directory (line 104) | def host_directory
method apps_directory (line 108) | def apps_directory
method apps_container_directory (line 112) | def apps_container_directory
method apps_volume (line 116) | def apps_volume
method apps_volume_args (line 122) | def apps_volume_args
method app_directory (line 126) | def app_directory
method app_container_directory (line 130) | def app_container_directory
method format_bind_ip (line 135) | def format_bind_ip(ip)
FILE: lib/kamal/configuration/registry.rb
class Kamal::Configuration::Registry (line 1) | class Kamal::Configuration::Registry
method initialize (line 4) | def initialize(config:, secrets:, context: "registry")
method server (line 10) | def server
method username (line 14) | def username
method password (line 18) | def password
method local? (line 22) | def local?
method local_port (line 26) | def local_port
method lookup (line 33) | def lookup(key)
FILE: lib/kamal/configuration/role.rb
class Kamal::Configuration::Role (line 1) | class Kamal::Configuration::Role
method initialize (line 10) | def initialize(name, config:)
method primary_host (line 30) | def primary_host
method hosts (line 34) | def hosts
method env_tags (line 38) | def env_tags(host)
method cmd (line 42) | def cmd
method option_args (line 46) | def option_args
method labels (line 54) | def labels
method label_args (line 58) | def label_args
method logging_args (line 62) | def logging_args
method logging (line 66) | def logging
method proxy (line 70) | def proxy
method running_proxy? (line 74) | def running_proxy?
method ssl? (line 78) | def ssl?
method stop_args (line 82) | def stop_args
method env (line 89) | def env(host)
method env_args (line 94) | def env_args(host)
method env_directory (line 98) | def env_directory
method secrets_io (line 102) | def secrets_io(host)
method secrets_path (line 106) | def secrets_path
method asset_volume_args (line 110) | def asset_volume_args
method primary? (line 115) | def primary?
method container_name (line 120) | def container_name(version = nil)
method container_prefix (line 124) | def container_prefix
method asset_path (line 129) | def asset_path
method assets? (line 133) | def assets?
method asset_volume (line 137) | def asset_volume(version = config.version)
method asset_path_options (line 144) | def asset_path_options
method asset_extracted_directory (line 148) | def asset_extracted_directory(version = config.version)
method asset_volume_directory (line 152) | def asset_volume_directory(version = config.version)
method ensure_one_host_for_ssl (line 156) | def ensure_one_host_for_ssl
method initialize_specialized_proxy (line 163) | def initialize_specialized_proxy
method tagged_hosts (line 186) | def tagged_hosts
method extract_hosts_from_config (line 199) | def extract_hosts_from_config
method default_labels (line 208) | def default_labels
method specializations (line 212) | def specializations
method role_config (line 216) | def role_config
method custom_labels (line 220) | def custom_labels
method asset_path_config (line 227) | def asset_path_config
FILE: lib/kamal/configuration/servers.rb
class Kamal::Configuration::Servers (line 1) | class Kamal::Configuration::Servers
method initialize (line 6) | def initialize(config:)
method role_names (line 15) | def role_names
FILE: lib/kamal/configuration/ssh.rb
class Kamal::Configuration::Ssh (line 1) | class Kamal::Configuration::Ssh
method initialize (line 8) | def initialize(config:)
method user (line 14) | def user
method port (line 18) | def port
method proxy (line 22) | def proxy
method keys_only (line 30) | def keys_only
method keys (line 34) | def keys
method key_data (line 38) | def key_data
method config (line 52) | def config
method options (line 56) | def options
method to_h (line 60) | def to_h
method logger (line 65) | def logger
method log_level (line 69) | def log_level
FILE: lib/kamal/configuration/sshkit.rb
class Kamal::Configuration::Sshkit (line 1) | class Kamal::Configuration::Sshkit
method initialize (line 6) | def initialize(config:)
method max_concurrent_starts (line 11) | def max_concurrent_starts
method pool_idle_timeout (line 15) | def pool_idle_timeout
method dns_retries (line 19) | def dns_retries
method to_h (line 23) | def to_h
FILE: lib/kamal/configuration/validation.rb
type Kamal::Configuration::Validation (line 4) | module Kamal::Configuration::Validation
function validation_doc (line 8) | def validation_doc
function validation_config_key (line 12) | def validation_config_key
function validate! (line 17) | def validate!(config, example: nil, context: nil, with: Kamal::Configu...
function validation_yml (line 24) | def validation_yml
FILE: lib/kamal/configuration/validator.rb
class Kamal::Configuration::Validator (line 1) | class Kamal::Configuration::Validator
method initialize (line 4) | def initialize(config, example:, context:)
method validate! (line 10) | def validate!
method validate_against_example! (line 15) | def validate_against_example!(validation_config, example)
method valid_type? (line 64) | def valid_type?(value, type)
method type_description (line 70) | def type_description(type)
method boolean? (line 80) | def boolean?(type)
method stringish? (line 84) | def stringish?(value)
method validate_array_of_or_type! (line 88) | def validate_array_of_or_type!(value, type)
method validate_array_of! (line 98) | def validate_array_of!(array, type)
method validate_hash_of! (line 108) | def validate_hash_of!(hash, type)
method validate_servers! (line 118) | def validate_servers!(servers)
method validate_ssh_config! (line 138) | def validate_ssh_config!(config)
method validate_paths! (line 148) | def validate_paths!(paths)
method validate_hooks_output! (line 166) | def validate_hooks_output!(value)
method validate_type! (line 179) | def validate_type!(value, *types)
method error (line 183) | def error(message)
method type_error (line 187) | def type_error(*expected_types)
method unknown_keys_error (line 192) | def unknown_keys_error(unknown_keys)
method error_context (line 196) | def error_context
method with_context (line 200) | def with_context(context)
method allow_extensions? (line 208) | def allow_extensions?
method extension? (line 212) | def extension?(key)
method check_unknown_keys! (line 216) | def check_unknown_keys!(config, example)
method validate_labels! (line 222) | def validate_labels!(labels)
method validate_docker_options! (line 234) | def validate_docker_options!(options)
FILE: lib/kamal/configuration/validator/accessory.rb
class Kamal::Configuration::Validator::Accessory (line 1) | class Kamal::Configuration::Validator::Accessory < Kamal::Configuration:...
method validate! (line 2) | def validate!
FILE: lib/kamal/configuration/validator/alias.rb
class Kamal::Configuration::Validator::Alias (line 1) | class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Val...
method validate! (line 2) | def validate!
FILE: lib/kamal/configuration/validator/builder.rb
class Kamal::Configuration::Validator::Builder (line 1) | class Kamal::Configuration::Validator::Builder < Kamal::Configuration::V...
method validate! (line 2) | def validate!
FILE: lib/kamal/configuration/validator/configuration.rb
class Kamal::Configuration::Validator::Configuration (line 1) | class Kamal::Configuration::Validator::Configuration < Kamal::Configurat...
method allow_extensions? (line 3) | def allow_extensions?
FILE: lib/kamal/configuration/validator/env.rb
class Kamal::Configuration::Validator::Env (line 1) | class Kamal::Configuration::Validator::Env < Kamal::Configuration::Valid...
method validate! (line 4) | def validate!
method validate_simple_env! (line 13) | def validate_simple_env!
method validate_complex_env! (line 17) | def validate_complex_env!
method known_keys (line 25) | def known_keys
method unknown_keys (line 29) | def unknown_keys
method validate_tags! (line 33) | def validate_tags!
FILE: lib/kamal/configuration/validator/proxy.rb
class Kamal::Configuration::Validator::Proxy (line 1) | class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Val...
method validate! (line 2) | def validate!
method ensure_valid_bind_ips (line 39) | def ensure_valid_bind_ips(bind_ips)
FILE: lib/kamal/configuration/validator/registry.rb
class Kamal::Configuration::Validator::Registry (line 1) | class Kamal::Configuration::Validator::Registry < Kamal::Configuration::...
method validate! (line 4) | def validate!
method validate_string_or_one_item_array! (line 14) | def validate_string_or_one_item_array!(key)
FILE: lib/kamal/configuration/validator/role.rb
class Kamal::Configuration::Validator::Role (line 1) | class Kamal::Configuration::Validator::Role < Kamal::Configuration::Vali...
method validate! (line 2) | def validate!
FILE: lib/kamal/configuration/validator/servers.rb
class Kamal::Configuration::Validator::Servers (line 1) | class Kamal::Configuration::Validator::Servers < Kamal::Configuration::V...
method validate! (line 2) | def validate!
FILE: lib/kamal/configuration/volume.rb
class Kamal::Configuration::Volume (line 1) | class Kamal::Configuration::Volume
method initialize (line 5) | def initialize(host_path:, container_path:, options: nil)
method docker_args (line 11) | def docker_args
method docker_args_string (line 15) | def docker_args_string
method host_path_for_docker_volume (line 22) | def host_path_for_docker_volume
FILE: lib/kamal/docker.rb
type Kamal::Docker (line 4) | module Kamal::Docker
function included_files (line 8) | def included_files
FILE: lib/kamal/env_file.rb
class Kamal::EnvFile (line 2) | class Kamal::EnvFile
method initialize (line 3) | def initialize(env)
method to_s (line 7) | def to_s
method to_io (line 18) | def to_io
method docker_env_file_line (line 25) | def docker_env_file_line(key, value)
method escape_docker_env_file_value (line 30) | def escape_docker_env_file_value(value)
method escape_docker_env_file_ascii_value (line 37) | def escape_docker_env_file_ascii_value(value)
FILE: lib/kamal/git.rb
type Kamal::Git (line 1) | module Kamal::Git
function used? (line 4) | def used?
function user_name (line 8) | def user_name
function email (line 12) | def email
function revision (line 16) | def revision
function uncommitted_changes (line 20) | def uncommitted_changes
function root (line 24) | def root
function uncommitted_files (line 29) | def uncommitted_files
function untracked_files (line 34) | def untracked_files
FILE: lib/kamal/secrets.rb
class Kamal::Secrets (line 3) | class Kamal::Secrets
method initialize (line 6) | def initialize(destination: nil, secrets_path:)
method [] (line 12) | def [](key)
method to_h (line 22) | def to_h
method secrets_files (line 26) | def secrets_files
method key? (line 30) | def key?(key)
method secrets (line 37) | def secrets
method secrets_filenames (line 43) | def secrets_filenames
method synchronized_fetch (line 47) | def synchronized_fetch(key)
FILE: lib/kamal/secrets/adapters.rb
type Kamal::Secrets::Adapters (line 2) | module Kamal::Secrets::Adapters
function lookup (line 3) | def self.lookup(name)
function adapter_class (line 11) | def self.adapter_class(name)
FILE: lib/kamal/secrets/adapters/aws_secrets_manager.rb
class Kamal::Secrets::Adapters::AwsSecretsManager (line 1) | class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adap...
method requires_account? (line 2) | def requires_account?
method login (line 7) | def login(_account)
method fetch_secrets (line 11) | def fetch_secrets(secrets, from:, account: nil, session:)
method get_from_secrets_manager (line 26) | def get_from_secrets_manager(secrets, account: nil)
method check_dependencies! (line 43) | def check_dependencies!
method cli_installed? (line 47) | def cli_installed?
FILE: lib/kamal/secrets/adapters/base.rb
class Kamal::Secrets::Adapters::Base (line 1) | class Kamal::Secrets::Adapters::Base
method fetch (line 4) | def fetch(secrets, account: nil, from: nil)
method requires_account? (line 13) | def requires_account?
method login (line 18) | def login(...)
method fetch_secrets (line 22) | def fetch_secrets(...)
method check_dependencies! (line 26) | def check_dependencies!
method prefixed_secrets (line 30) | def prefixed_secrets(secrets, from:)
FILE: lib/kamal/secrets/adapters/bitwarden.rb
class Kamal::Secrets::Adapters::Bitwarden (line 1) | class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
method login (line 3) | def login(account)
method fetch_secrets (line 24) | def fetch_secrets(secrets, from:, account:, session:)
method fetch_secrets_from_fields (line 44) | def fetch_secrets_from_fields(fields, item, item_json)
method items_fields (line 53) | def items_fields(secrets)
method signedin? (line 63) | def signedin?(account)
method run_command (line 67) | def run_command(command, session: nil, raw: false)
method check_dependencies! (line 73) | def check_dependencies!
method cli_installed? (line 77) | def cli_installed?
FILE: lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
class Kamal::Secrets::Adapters::BitwardenSecretsManager (line 1) | class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets...
method requires_account? (line 2) | def requires_account?
method fetch_secrets (line 12) | def fetch_secrets(secrets, from:, account:, session:)
method extract_command_and_project (line 37) | def extract_command_and_project(secrets)
method run_command (line 48) | def run_command(command, session: nil)
method login (line 53) | def login(account)
method check_dependencies! (line 58) | def check_dependencies!
method cli_installed? (line 62) | def cli_installed?
FILE: lib/kamal/secrets/adapters/doppler.rb
class Kamal::Secrets::Adapters::Doppler (line 1) | class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
method requires_account? (line 2) | def requires_account?
method login (line 7) | def login(*)
method loggedin? (line 14) | def loggedin?
method fetch_secrets (line 19) | def fetch_secrets(secrets, from:, **)
method secrets_get_flags (line 33) | def secrets_get_flags(secrets)
method service_token_set? (line 45) | def service_token_set?
method check_dependencies! (line 49) | def check_dependencies!
method cli_installed? (line 53) | def cli_installed?
FILE: lib/kamal/secrets/adapters/enpass.rb
class Kamal::Secrets::Adapters::Enpass (line 11) | class Kamal::Secrets::Adapters::Enpass < Kamal::Secrets::Adapters::Base
method requires_account? (line 12) | def requires_account?
method fetch_secrets (line 17) | def fetch_secrets(secrets, from:, account:, session:)
method check_dependencies! (line 25) | def check_dependencies!
method cli_installed? (line 29) | def cli_installed?
method login (line 34) | def login(account)
method fetch_secret_titles (line 38) | def fetch_secret_titles(secrets)
method parse_result_and_take_secrets (line 51) | def parse_result_and_take_secrets(unparsed_result, secrets)
FILE: lib/kamal/secrets/adapters/gcp_secret_manager.rb
class Kamal::Secrets::Adapters::GcpSecretManager (line 1) | class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapt...
method login (line 3) | def login(account)
method fetch_secrets (line 29) | def fetch_secrets(secrets, from:, account:, session:)
method fetch_secret (line 41) | def fetch_secret(project, secret_name, secret_version, user, service_a...
method secrets_with_metadata (line 66) | def secrets_with_metadata(secrets)
method run_command (line 80) | def run_command(command, project: "default", user: "default", service_...
method check_dependencies! (line 92) | def check_dependencies!
method cli_installed? (line 96) | def cli_installed?
method logged_in? (line 101) | def logged_in?
method parse_account (line 105) | def parse_account(account)
method is_user? (line 109) | def is_user?(candidate)
FILE: lib/kamal/secrets/adapters/last_pass.rb
class Kamal::Secrets::Adapters::LastPass (line 1) | class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
method login (line 3) | def login(account)
method loggedin? (line 10) | def loggedin?(account)
method fetch_secrets (line 14) | def fetch_secrets(secrets, from:, account:, session:)
method check_dependencies! (line 32) | def check_dependencies!
method cli_installed? (line 36) | def cli_installed?
FILE: lib/kamal/secrets/adapters/one_password.rb
class Kamal::Secrets::Adapters::OnePassword (line 1) | class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::...
method login (line 5) | def login(account)
method loggedin? (line 13) | def loggedin?(account)
method fetch_secrets (line 18) | def fetch_secrets(secrets, from:, account:, session:)
method fetch_specified_secrets (line 26) | def fetch_specified_secrets(secrets, from:, account:, session:)
method fetch_all_secrets (line 39) | def fetch_all_secrets(from:, account:, session:)
method to_options (line 51) | def to_options(**options)
method vaults_items_fields (line 55) | def vaults_items_fields(secrets)
method vault_items (line 69) | def vault_items(from)
method fields_map (line 75) | def fields_map(fields_json)
method op_item_get (line 83) | def op_item_get(vault, item, fields: nil, account:, session:)
method check_dependencies! (line 96) | def check_dependencies!
method cli_installed? (line 100) | def cli_installed?
FILE: lib/kamal/secrets/adapters/passbolt.rb
class Kamal::Secrets::Adapters::Passbolt (line 1) | class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base
method requires_account? (line 2) | def requires_account?
method login (line 8) | def login(*)
method fetch_secrets (line 13) | def fetch_secrets(secrets, from:, **)
method secrets_get_folders (line 60) | def secrets_get_folders(secrets)
method get_folder_path (line 111) | def get_folder_path(folder, all_folders, path = [])
method check_dependencies! (line 121) | def check_dependencies!
method cli_installed? (line 125) | def cli_installed?
FILE: lib/kamal/secrets/adapters/test.rb
class Kamal::Secrets::Adapters::Test (line 1) | class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
method login (line 3) | def login(account)
method fetch_secrets (line 7) | def fetch_secrets(secrets, from:, account:, session:)
method check_dependencies! (line 13) | def check_dependencies!
FILE: lib/kamal/secrets/dotenv/inline_command_substitution.rb
class Kamal::Secrets::Dotenv::InlineCommandSubstitution (line 1) | class Kamal::Secrets::Dotenv::InlineCommandSubstitution
method install! (line 17) | def install!
method call (line 21) | def call(value, env, overwrite: false)
method inline_secrets_command (line 43) | def inline_secrets_command(command)
FILE: lib/kamal/sshkit_with_ext.rb
class SSHKit::Backend::Abstract (line 9) | class SSHKit::Backend::Abstract
method capture_with_info (line 10) | def capture_with_info(*args, **kwargs)
method capture_with_debug (line 14) | def capture_with_debug(*args, **kwargs)
method capture_with_pretty_json (line 18) | def capture_with_pretty_json(*args, **kwargs)
method puts_by_host (line 22) | def puts_by_host(host, output, type: "App", quiet: false)
type CommandEnvMerge (line 35) | module CommandEnvMerge
function command (line 40) | def command(args, options)
function build_command (line 48) | def build_command(args, env: nil, **options)
function default_command_options (line 53) | def default_command_options
function env_for (line 57) | def env_for(env)
class SSHKit::Backend::Netssh::Configuration (line 64) | class SSHKit::Backend::Netssh::Configuration
class SSHKit::Backend::Netssh (line 68) | class SSHKit::Backend::Netssh
type DnsRetriable (line 69) | module DnsRetriable
function with_dns_retry (line 75) | def with_dns_retry(hostname, retries: config.dns_retries, base: DNS_...
function retryable_dns_error? (line 91) | def retryable_dns_error?(error)
function dns_retry_sleep (line 102) | def dns_retry_sleep(attempt, base:, jitter:, max_sleep:)
type LimitConcurrentStartsClass (line 109) | module LimitConcurrentStartsClass
function configure (line 112) | def configure(&block)
type ConnectSsh (line 126) | module ConnectSsh
function connect_ssh (line 128) | def connect_ssh(...)
type DnsRetriableConnection (line 134) | module DnsRetriableConnection
function connect_ssh (line 136) | def connect_ssh(...)
type LimitConcurrentStartsInstance (line 142) | module LimitConcurrentStartsInstance
function with_ssh (line 144) | def with_ssh(&block)
function connect_ssh (line 155) | def connect_ssh(...)
function with_concurrency_limit (line 159) | def with_concurrency_limit(&block)
class SSHKit::Runner::Parallel (line 170) | class SSHKit::Runner::Parallel
type CompleteAll (line 176) | module CompleteAll
function execute (line 177) | def execute
type NetSshForwardingNoPuts (line 207) | module NetSshForwardingNoPuts
function puts (line 208) | def puts(*)
type SSHKitDslRoles (line 214) | module SSHKitDslRoles
function on_roles (line 230) | def on_roles(roles, hosts:, parallel: true, &block)
FILE: lib/kamal/tags.rb
class Kamal::Tags (line 3) | class Kamal::Tags
method from_config (line 7) | def from_config(config, **extra)
method default_tags (line 11) | def default_tags(config)
method service_version (line 20) | def service_version(config)
method initialize (line 25) | def initialize(**tags)
method env (line 29) | def env
method to_s (line 33) | def to_s
method except (line 37) | def except(*tags)
FILE: lib/kamal/utils.rb
type Kamal::Utils (line 3) | module Kamal::Utils
function argumentize (line 9) | def argumentize(argument, attributes, sensitive: false)
function optionize (line 24) | def optionize(args, with: nil, escape: true)
function flatten_args (line 35) | def flatten_args(args)
function sensitive (line 42) | def sensitive(...)
function redacted (line 46) | def redacted(value)
function escape_shell_value (line 60) | def escape_shell_value(value)
function escape_ascii_shell_value (line 66) | def escape_ascii_shell_value(value)
function filter_specific_items (line 73) | def filter_specific_items(filters, items)
function stable_sort! (line 87) | def stable_sort!(elements, &block)
function join_commands (line 91) | def join_commands(commands)
function docker_arch (line 95) | def docker_arch
function older_version? (line 107) | def older_version?(version, other_version)
FILE: lib/kamal/utils/sensitive.rb
class Kamal::Utils::Sensitive (line 4) | class Kamal::Utils::Sensitive
method initialize (line 12) | def initialize(value, redaction: "[REDACTED]")
method encode_with (line 17) | def encode_with(coder)
FILE: lib/kamal/version.rb
type Kamal (line 1) | module Kamal
FILE: test/cli/accessory_test.rb
class CliAccessoryTest (line 3) | class CliAccessoryTest < CliTestCase
method run_command (line 277) | def run_command(*command)
FILE: test/cli/app_test.rb
class CliAppTest (line 3) | class CliAppTest < CliTestCase
method run_command (line 583) | def run_command(*command, config: :with_accessories, host: "1.1.1.1", ...
method stub_running (line 591) | def stub_running
FILE: test/cli/build_test.rb
class CliBuildTest (line 3) | class CliBuildTest < CliTestCase
method run_command (line 454) | def run_command(*command, fixture: :with_accessories)
method stub_dependency_checks (line 458) | def stub_dependency_checks
FILE: test/cli/cli_test_case.rb
class CliTestCase (line 3) | class CliTestCase < ActiveSupport::TestCase
method fail_hook (line 19) | def fail_hook(hook)
method stub_setup (line 30) | def stub_setup
method assert_hook_ran (line 43) | def assert_hook_ran(hook, output, count: 1)
method with_argv (line 48) | def with_argv(*argv)
method with_build_directory (line 56) | def with_build_directory
method pwd_sha (line 65) | def pwd_sha
FILE: test/cli/lock_test.rb
class CliLockTest (line 3) | class CliLockTest < CliTestCase
method run_command (line 17) | def run_command(*command)
FILE: test/cli/main_test.rb
class CliMainTest (line 3) | class CliMainTest < CliTestCase
method run_command (line 728) | def run_command(*command, config_file: "deploy_simple")
method run_command_with_config_path (line 734) | def run_command_with_config_path(*command, config_path:, destination: ...
method in_dummy_git_repo (line 744) | def in_dummy_git_repo
method with_config_files (line 753) | def with_config_files
method assert_file (line 767) | def assert_file(file, content)
method with_kamal_lock_env (line 771) | def with_kamal_lock_env
FILE: test/cli/proxy_test.rb
class CliProxyTest (line 3) | class CliProxyTest < CliTestCase
method run_command (line 408) | def run_command(*command, fixture: :with_proxy)
FILE: test/cli/prune_test.rb
class CliPruneTest (line 3) | class CliPruneTest < CliTestCase
method run_command (line 33) | def run_command(*command)
FILE: test/cli/registry_test.rb
class CliRegistryTest (line 3) | class CliRegistryTest < CliTestCase
method run_command (line 136) | def run_command(*command, fixture: :with_accessories)
FILE: test/cli/secrets_test.rb
class CliSecretsTest (line 3) | class CliSecretsTest < CliTestCase
method run_command (line 31) | def run_command(*command)
FILE: test/cli/server_test.rb
class CliServerTest (line 3) | class CliServerTest < CliTestCase
method run_command (line 92) | def run_command(*command)
FILE: test/commander_test.rb
class CommanderTest (line 3) | class CommanderTest < ActiveSupport::TestCase
method configure_with (line 196) | def configure_with(variant)
FILE: test/commands/accessory_test.rb
class CommandsAccessoryTest (line 3) | class CommandsAccessoryTest < ActiveSupport::TestCase
method new_command (line 202) | def new_command(accessory)
FILE: test/commands/app_test.rb
class CommandsAppTest (line 3) | class CommandsAppTest < ActiveSupport::TestCase
method new_command (line 575) | def new_command(role: "web", host: "1.1.1.1", **additional_config)
FILE: test/commands/auditor_test.rb
class CommandsAuditorTest (line 4) | class CommandsAuditorTest < ActiveSupport::TestCase
method new_command (line 61) | def new_command(destination: nil, **details)
FILE: test/commands/builder_test.rb
class CommandsBuilderTest (line 3) | class CommandsBuilderTest < ActiveSupport::TestCase
method new_builder_command (line 275) | def new_builder_command(additional_config = {})
method local_arch (line 283) | def local_arch
method remote_arch (line 287) | def remote_arch
FILE: test/commands/docker_test.rb
class CommandsDockerTest (line 3) | class CommandsDockerTest < ActiveSupport::TestCase
FILE: test/commands/hook_test.rb
class CommandsHookTest (line 3) | class CommandsHookTest < ActiveSupport::TestCase
method new_command (line 51) | def new_command(**extra_config)
FILE: test/commands/lock_test.rb
class CommandsLockTest (line 3) | class CommandsLockTest < ActiveSupport::TestCase
method new_command (line 30) | def new_command
FILE: test/commands/proxy_test.rb
class CommandsProxyTest (line 3) | class CommandsProxyTest < ActiveSupport::TestCase
method new_command (line 225) | def new_command
FILE: test/commands/prune_test.rb
class CommandsPruneTest (line 3) | class CommandsPruneTest < ActiveSupport::TestCase
method new_command (line 34) | def new_command
FILE: test/commands/registry_test.rb
class CommandsRegistryTest (line 3) | class CommandsRegistryTest < ActiveSupport::TestCase
method registry (line 98) | def registry
method main_config (line 102) | def main_config
method accessory_registry_config (line 106) | def accessory_registry_config
FILE: test/commands/server_test.rb
class CommandsServerTest (line 3) | class CommandsServerTest < ActiveSupport::TestCase
method new_command (line 16) | def new_command(extra_config = {})
FILE: test/configuration/accessory_test.rb
class ConfigurationAccessoryTest (line 3) | class ConfigurationAccessoryTest < ActiveSupport::TestCase
FILE: test/configuration/boot_test.rb
class ConfigurationBootTest (line 3) | class ConfigurationBootTest < ActiveSupport::TestCase
method config_with_boot (line 40) | def config_with_boot(boot)
FILE: test/configuration/builder_test.rb
class ConfigurationBuilderTest (line 3) | class ConfigurationBuilderTest < ActiveSupport::TestCase
method config (line 192) | def config
FILE: test/configuration/env/tags_test.rb
class ConfigurationEnvTagsTest (line 3) | class ConfigurationEnvTagsTest < ActiveSupport::TestCase
FILE: test/configuration/env_test.rb
class ConfigurationEnvTest (line 3) | class ConfigurationEnvTest < ActiveSupport::TestCase
method assert_config (line 71) | def assert_config(config:, clear: {}, secrets: {})
FILE: test/configuration/proxy/boot_test.rb
class ConfigurationProxyBootTest (line 3) | class ConfigurationProxyBootTest < ActiveSupport::TestCase
FILE: test/configuration/proxy_test.rb
class ConfigurationProxyTest (line 3) | class ConfigurationProxyTest < ActiveSupport::TestCase
method config (line 109) | def config
FILE: test/configuration/role_test.rb
class ConfigurationRoleTest (line 3) | class ConfigurationRoleTest < ActiveSupport::TestCase
method config (line 298) | def config
method config_with_roles (line 302) | def config_with_roles
FILE: test/configuration/ssh_test.rb
class ConfigurationSshTest (line 3) | class ConfigurationSshTest < ActiveSupport::TestCase
FILE: test/configuration/sshkit_test.rb
class ConfigurationSshkitTest (line 3) | class ConfigurationSshkitTest < ActiveSupport::TestCase
FILE: test/configuration/validation_test.rb
class ConfigurationValidationTest (line 2) | class ConfigurationValidationTest < ActiveSupport::TestCase
method assert_error (line 124) | def assert_error(message, **invalid_config)
FILE: test/configuration/volume_test.rb
class ConfigurationVolumeTest (line 3) | class ConfigurationVolumeTest < ActiveSupport::TestCase
FILE: test/configuration_test.rb
class ConfigurationTest (line 3) | class ConfigurationTest < ActiveSupport::TestCase
FILE: test/env_file_test.rb
class EnvFileTest (line 3) | class EnvFileTest < ActiveSupport::TestCase
FILE: test/git_test.rb
class GitTest (line 3) | class GitTest < ActiveSupport::TestCase
FILE: test/integration/accessory_test.rb
class AccessoryTest (line 3) | class AccessoryTest < IntegrationTest
method assert_accessory_running (line 57) | def assert_accessory_running(name)
method assert_accessory_not_running (line 61) | def assert_accessory_not_running(name)
method assert_accessory_volume_mount_options (line 65) | def assert_accessory_volume_mount_options(name)
method assert_accessory_file_mode_and_owner (line 70) | def assert_accessory_file_mode_and_owner(name)
method assert_accessory_directory_mode_and_owner (line 75) | def assert_accessory_directory_mode_and_owner(name)
method accessory_details (line 80) | def accessory_details(name)
method assert_netcat_is_up (line 84) | def assert_netcat_is_up
method assert_netcat_not_found (line 90) | def assert_netcat_not_found
method netcat_response (line 96) | def netcat_response
FILE: test/integration/app_test.rb
class AppTest (line 3) | class AppTest < IntegrationTest
FILE: test/integration/broken_deploy_test.rb
class BrokenDeployTest (line 3) | class BrokenDeployTest < IntegrationTest
method assert_failed_deploy (line 25) | def assert_failed_deploy(output)
FILE: test/integration/integration_test.rb
class IntegrationTest (line 4) | class IntegrationTest < ActiveSupport::TestCase
method docker_compose (line 25) | def docker_compose(*commands, capture: false, raise_on_error: true)
method deployer_exec (line 38) | def deployer_exec(*commands, workdir: nil, **options)
method kamal (line 43) | def kamal(*commands, **options)
method assert_app_is_down (line 47) | def assert_app_is_down
method assert_app_in_maintenance (line 51) | def assert_app_in_maintenance(message: nil)
method assert_app_not_found (line 55) | def assert_app_not_found
method assert_app_error_code (line 59) | def assert_app_error_code(code, message: nil)
method assert_app_is_up (line 66) | def assert_app_is_up(version: nil, app: @app, cert: nil)
method wait_for_app_to_be_up (line 73) | def wait_for_app_to_be_up(timeout: 20, up_count: 3)
method app_response (line 85) | def app_response(app: @app, cert: nil)
method update_app_rev (line 95) | def update_app_rev
method break_app (line 100) | def break_app
method latest_app_version (line 105) | def latest_app_version
method assert_app_version (line 109) | def assert_app_version(version, response)
method assert_hooks_ran (line 113) | def assert_hooks_ran(*hooks)
method assert_200 (line 120) | def assert_200(response)
method wait_for_healthy (line 132) | def wait_for_healthy(timeout: 30)
method setup_deployer (line 147) | def setup_deployer
method debug_response_code (line 152) | def debug_response_code(app_response, expected_code)
method assert_container_running (line 163) | def assert_container_running(host:, name:)
method assert_container_not_running (line 167) | def assert_container_not_running(host:, name:)
method container_running? (line 171) | def container_running?(host:, name:)
method assert_app_directory_removed (line 175) | def assert_app_directory_removed
method assert_directory_removed (line 179) | def assert_directory_removed(directory)
method assert_proxy_running (line 183) | def assert_proxy_running
method assert_proxy_not_running (line 187) | def assert_proxy_not_running
method app_host (line 191) | def app_host(app = @app)
method https_response_with_cert (line 200) | def https_response_with_cert(uri, cert)
FILE: test/integration/lock_test.rb
class LockTest (line 3) | class LockTest < IntegrationTest
FILE: test/integration/main_test.rb
class MainTest (line 3) | class MainTest < IntegrationTest
method assert_envs (line 177) | def assert_envs(version:)
method assert_env (line 192) | def assert_env(key, value, vm:, version:)
method assert_no_env (line 196) | def assert_no_env(key, vm:, version:)
method assert_accumulated_assets (line 202) | def assert_accumulated_assets(*versions)
method assert_asset_volume_read_only (line 210) | def assert_asset_volume_read_only(version)
method image_ids (line 215) | def image_ids(vm:)
method container_ids (line 219) | def container_ids(vm:)
method assert_no_images_or_containers (line 223) | def assert_no_images_or_containers
method assert_images_and_containers (line 230) | def assert_images_and_containers
method assert_hook_env_variables (line 237) | def assert_hook_env_variables(output, version:)
method assert_hook_output (line 248) | def assert_hook_output(output)
FILE: test/integration/proxy_test.rb
class ProxyTest (line 3) | class ProxyTest < IntegrationTest
method assert_docker_options_in_file (line 54) | def assert_docker_options_in_file
method assert_docker_options_in_container (line 59) | def assert_docker_options_in_container
FILE: test/secrets/aws_secrets_manager_adapter_test.rb
class AwsSecretsManagerAdapterTest (line 3) | class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
method run_command (line 191) | def run_command(*command, account: "default")
FILE: test/secrets/bitwarden_adapter_test.rb
class BitwardenAdapterTest (line 3) | class BitwardenAdapterTest < SecretAdapterTestCase
method run_command (line 194) | def run_command(*command)
method stub_unlocked (line 204) | def stub_unlocked
method stub_mypassword (line 212) | def stub_mypassword(session: nil)
method stub_noteitem (line 235) | def stub_noteitem(session: nil)
method stub_noteitem_with_fields (line 259) | def stub_noteitem_with_fields(session: nil)
method stub_myitem (line 289) | def stub_myitem
FILE: test/secrets/bitwarden_secrets_manager_adapter_test.rb
class BitwardenSecretsManagerAdapterTest (line 3) | class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
method stub_login (line 182) | def stub_login
method run_command (line 186) | def run_command(*command)
FILE: test/secrets/doppler_adapter_test.rb
class DopplerAdapterTest (line 3) | class DopplerAdapterTest < SecretAdapterTestCase
method run_command (line 166) | def run_command(*command)
method single_item_json (line 175) | def single_item_json
FILE: test/secrets/dotenv_inline_command_substitution_test.rb
class SecretsInlineCommandSubstitution (line 3) | class SecretsInlineCommandSubstitution < SecretAdapterTestCase
FILE: test/secrets/enpass_adapter_test.rb
class EnpassAdapterTest (line 3) | class EnpassAdapterTest < SecretAdapterTestCase
method run_command (line 72) | def run_command(*command)
FILE: test/secrets/gcp_secret_manager_adapter_test.rb
class GcpSecretManagerAdapterTest (line 3) | class GcpSecretManagerAdapterTest < SecretAdapterTestCase
method run_command (line 141) | def run_command(*command, account: "default")
method stub_gcloud_version (line 151) | def stub_gcloud_version(succeed: true)
method stub_authenticated (line 155) | def stub_authenticated
method stub_unauthenticated (line 168) | def stub_unauthenticated
method stub_mypassword (line 183) | def stub_mypassword
method stub_items (line 197) | def stub_items(n, project: nil, account: nil, version: "latest", imper...
FILE: test/secrets/last_pass_adapter_test.rb
class LastPassAdapterTest (line 3) | class LastPassAdapterTest < SecretAdapterTestCase
method run_command (line 137) | def run_command(*command)
method single_item_json (line 147) | def single_item_json
FILE: test/secrets/one_password_adapter_test.rb
class SecretsOnePasswordAdapterTest (line 3) | class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
method run_command (line 224) | def run_command(*command)
method single_item_json (line 234) | def single_item_json
FILE: test/secrets/passbolt_adapter_test.rb
class PassboltAdapterTest (line 3) | class PassboltAdapterTest < SecretAdapterTestCase
method run_command (line 481) | def run_command(*command)
FILE: test/secrets_test.rb
class SecretsTest (line 3) | class SecretsTest < ActiveSupport::TestCase
FILE: test/sshkit_dns_retry_test.rb
class SshkitDnsRetryTest (line 3) | class SshkitDnsRetryTest < ActiveSupport::TestCase
FILE: test/test_helper.rb
class SSHKit::Backend::Printer (line 22) | class SSHKit::Backend::Printer
method upload! (line 23) | def upload!(local, location, **kwargs)
type SSHKit (line 31) | module SSHKit
type DSL (line 32) | module DSL
function run_locally (line 33) | def run_locally(&block)
class ActiveSupport::TestCase (line 39) | class ActiveSupport::TestCase
method stdouted (line 44) | def stdouted
method stderred (line 48) | def stderred
method stub_stdin_tty (line 52) | def stub_stdin_tty
method stub_stdin_file (line 58) | def stub_stdin_file
method stub_stdin (line 64) | def stub_stdin(io)
method with_test_secrets (line 73) | def with_test_secrets(**files)
method setup_test_secrets (line 80) | def setup_test_secrets(**files)
method teardown_test_secrets (line 94) | def teardown_test_secrets
method with_error_pages (line 99) | def with_error_pages(directory:)
method copy_fixtures (line 117) | def copy_fixtures(to_dir)
class SecretAdapterTestCase (line 124) | class SecretAdapterTestCase < ActiveSupport::TestCase
method stub_ticks (line 130) | def stub_ticks
method stub_ticks_with (line 134) | def stub_ticks_with(command, succeed: true)
FILE: test/utils_test.rb
class UtilsTest (line 3) | class UtilsTest < ActiveSupport::TestCase
Condensed preview — 313 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (824K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 2308,
"preview": "name: CI\non:\n push:\n branches:\n - main\n pull_request:\n workflow_dispatch:\n\npermissions:\n contents: read\n\njob"
},
{
"path": ".github/workflows/docker-publish.yml",
"chars": 1602,
"preview": "name: Docker\n\non:\n workflow_dispatch:\n inputs:\n tagInput:\n description: 'Tag'\n required: true\n "
},
{
"path": ".gitignore",
"chars": 59,
"preview": ".byebug_history\n*.gem\ncoverage/*\n.DS_Store\ngemfiles/*.lock\n"
},
{
"path": ".rubocop.yml",
"chars": 50,
"preview": "inherit_gem:\n rubocop-rails-omakase: rubocop.yml\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 2852,
"preview": "# Contributor Code of Conduct\n\nAs contributors and maintainers of the Kamal project, we pledge to create a welcoming and"
},
{
"path": "CONTRIBUTING.md",
"chars": 2844,
"preview": "# Contributing to Kamal development\n\nThank you for considering contributing to Kamal! This document outlines some guidel"
},
{
"path": "Dockerfile",
"chars": 1208,
"preview": "FROM ruby:3.4-alpine\n\n# Install docker/buildx-bin\nCOPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/"
},
{
"path": "Gemfile",
"chars": 172,
"preview": "source \"https://rubygems.org\"\ngit_source(:github) { |repo| \"https://github.com/#{repo}.git\" }\n\ngemspec\n\ngroup :rubocop d"
},
{
"path": "MIT-LICENSE",
"chars": 1068,
"preview": "Copyright (c) 2023 David Heinemeier Hansson\n\nPermission is hereby granted, free of charge, to any person obtaining\na cop"
},
{
"path": "README.md",
"chars": 925,
"preview": "# Kamal: Deploy web apps anywhere\n\nFrom bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses"
},
{
"path": "bin/docs",
"chars": 3468,
"preview": "#!/usr/bin/env ruby\nrequire \"stringio\"\n\ndef usage\n puts \"Usage: #{$0} <kamal_site_repo>\"\n exit 1\nend\n\nusage if ARGV.si"
},
{
"path": "bin/kamal",
"chars": 428,
"preview": "#!/usr/bin/env ruby\n\n# Prevent failures from being reported twice.\nThread.report_on_exception = false\n\nrequire \"kamal\"\n\n"
},
{
"path": "bin/release",
"chars": 353,
"preview": "#!/usr/bin/env bash\n\nVERSION=$1\n\nprintf \"module Kamal\\n VERSION = \\\"$VERSION\\\"\\nend\\n\" > ./lib/kamal/version.rb\nbundle\n"
},
{
"path": "bin/test",
"chars": 116,
"preview": "#!/usr/bin/env ruby\n$: << File.expand_path(\"../test\", __dir__)\n\nrequire \"bundler/setup\"\nrequire \"rails/plugin/test\"\n"
},
{
"path": "gemfiles/rails_edge.gemfile",
"chars": 203,
"preview": "source 'https://rubygems.org'\ngit_source(:github) { |repo| \"https://github.com/#{repo}.git\" }\n\ngit \"https://github.com/r"
},
{
"path": "kamal.gemspec",
"chars": 1143,
"preview": "require_relative \"lib/kamal/version\"\n\nGem::Specification.new do |spec|\n spec.name = \"kamal\"\n spec.version ="
},
{
"path": "lib/kamal/cli/accessory.rb",
"chars": 12170,
"preview": "require \"active_support/core_ext/array/conversions\"\nrequire \"concurrent/array\"\n\nclass Kamal::Cli::Accessory < Kamal::Cli"
},
{
"path": "lib/kamal/cli/alias/command.rb",
"chars": 259,
"preview": "class Kamal::Cli::Alias::Command < Thor::DynamicCommand\n def run(instance, args = [])\n if (command = KAMAL.resolve_a"
},
{
"path": "lib/kamal/cli/app/assets.rb",
"chars": 583,
"preview": "class Kamal::Cli::App::Assets\n attr_reader :host, :role, :sshkit\n delegate :execute, :capture_with_info, :info, to: :s"
},
{
"path": "lib/kamal/cli/app/boot.rb",
"chars": 3920,
"preview": "class Kamal::Cli::App::Boot\n attr_reader :host, :role, :version, :barrier, :sshkit\n delegate :execute, :capture_with_i"
},
{
"path": "lib/kamal/cli/app/error_pages.rb",
"chars": 933,
"preview": "class Kamal::Cli::App::ErrorPages\n ERROR_PAGES_GLOB = \"{4??.html,5??.html}\"\n\n attr_reader :host, :sshkit\n delegate :u"
},
{
"path": "lib/kamal/cli/app/ssl_certificates.rb",
"chars": 807,
"preview": "class Kamal::Cli::App::SslCertificates\n attr_reader :host, :role, :sshkit\n delegate :execute, :info, :upload!, to: :ss"
},
{
"path": "lib/kamal/cli/app.rb",
"chars": 15063,
"preview": "class Kamal::Cli::App < Kamal::Cli::Base\n desc \"boot\", \"Boot app on servers (or reboot app if already running)\"\n def b"
},
{
"path": "lib/kamal/cli/base.rb",
"chars": 7284,
"preview": "require \"thor\"\nrequire \"kamal/sshkit_with_ext\"\n\nmodule Kamal::Cli\n class Base < Thor\n include SSHKit::DSL\n\n VERBO"
},
{
"path": "lib/kamal/cli/build/clone.rb",
"chars": 1898,
"preview": "class Kamal::Cli::Build::Clone\n attr_reader :sshkit\n delegate :info, :error, :execute, :capture_with_info, to: :sshkit"
},
{
"path": "lib/kamal/cli/build/port_forwarding.rb",
"chars": 1610,
"preview": "require \"concurrent/atomic/count_down_latch\"\n\nclass Kamal::Cli::Build::PortForwarding\n attr_reader :hosts, :port, :ssh_"
},
{
"path": "lib/kamal/cli/build.rb",
"chars": 7891,
"preview": "class Kamal::Cli::Build < Kamal::Cli::Base\n class BuildError < StandardError; end\n\n desc \"deliver\", \"Build app and pus"
},
{
"path": "lib/kamal/cli/healthcheck/barrier.rb",
"chars": 479,
"preview": "require \"concurrent/ivar\"\n\nclass Kamal::Cli::Healthcheck::Barrier\n def initialize\n @ivar = Concurrent::IVar.new\n en"
},
{
"path": "lib/kamal/cli/healthcheck/error.rb",
"chars": 57,
"preview": "class Kamal::Cli::Healthcheck::Error < StandardError\nend\n"
},
{
"path": "lib/kamal/cli/healthcheck/poller.rb",
"chars": 1101,
"preview": "module Kamal::Cli::Healthcheck::Poller\n extend self\n\n def wait_for_healthy(&block)\n attempt = 1\n timeout_at = Ti"
},
{
"path": "lib/kamal/cli/lock.rb",
"chars": 1100,
"preview": "class Kamal::Cli::Lock < Kamal::Cli::Base\n desc \"status\", \"Report lock status\"\n def status\n handle_missing_lock do\n"
},
{
"path": "lib/kamal/cli/main.rb",
"chars": 10078,
"preview": "class Kamal::Cli::Main < Kamal::Cli::Base\n desc \"setup\", \"Setup all accessories, push the env, and deploy app to server"
},
{
"path": "lib/kamal/cli/proxy.rb",
"chars": 12274,
"preview": "class Kamal::Cli::Proxy < Kamal::Cli::Base\n desc \"boot\", \"Boot proxy on servers\"\n def boot\n with_lock do\n on(K"
},
{
"path": "lib/kamal/cli/prune.rb",
"chars": 971,
"preview": "class Kamal::Cli::Prune < Kamal::Cli::Base\n desc \"all\", \"Prune unused images and stopped containers\"\n def all\n with"
},
{
"path": "lib/kamal/cli/registry.rb",
"chars": 2180,
"preview": "class Kamal::Cli::Registry < Kamal::Cli::Base\n desc \"setup\", \"Setup local registry or log in to remote registry locally"
},
{
"path": "lib/kamal/cli/secrets.rb",
"chars": 1699,
"preview": "class Kamal::Cli::Secrets < Kamal::Cli::Base\n desc \"fetch [SECRETS...]\", \"Fetch secrets from a vault\"\n option :adapter"
},
{
"path": "lib/kamal/cli/server.rb",
"chars": 2100,
"preview": "class Kamal::Cli::Server < Kamal::Cli::Base\n desc \"exec\", \"Run a custom command on the server (use --help to show optio"
},
{
"path": "lib/kamal/cli/templates/deploy.yml",
"chars": 2796,
"preview": "# Name of your application. Used to uniquely configure containers.\nservice: my-app\n\n# Name of the container image.\nimage"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/docker-setup.sample",
"chars": 51,
"preview": "#!/bin/sh\n\necho \"Docker set up on $KAMAL_HOSTS...\"\n"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/post-app-boot.sample",
"chars": 71,
"preview": "#!/bin/sh\n\necho \"Booted app version $KAMAL_VERSION on $KAMAL_HOSTS...\"\n"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/post-deploy.sample",
"chars": 319,
"preview": "#!/bin/sh\n\n# A sample post-deploy hook\n#\n# These environment variables are available:\n# KAMAL_RECORDED_AT\n# KAMAL_PERFOR"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample",
"chars": 55,
"preview": "#!/bin/sh\n\necho \"Rebooted kamal-proxy on $KAMAL_HOSTS\"\n"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample",
"chars": 72,
"preview": "#!/bin/sh\n\necho \"Booting app version $KAMAL_VERSION on $KAMAL_HOSTS...\"\n"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/pre-build.sample",
"chars": 1103,
"preview": "#!/bin/sh\n\n# A sample pre-build hook\n#\n# Checks:\n# 1. We have a clean checkout\n# 2. A remote is configured\n# 3. The bran"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/pre-connect.sample",
"chars": 1039,
"preview": "#!/usr/bin/env ruby\n\n# A sample pre-connect check\n#\n# Warms DNS before connecting to hosts in parallel\n#\n# These environ"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/pre-deploy.sample",
"chars": 2833,
"preview": "#!/usr/bin/env ruby\n\n# A sample pre-deploy hook\n#\n# Checks the Github status of the build, waiting for a pending build t"
},
{
"path": "lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample",
"chars": 59,
"preview": "#!/bin/sh\n\necho \"Rebooting kamal-proxy on $KAMAL_HOSTS...\"\n"
},
{
"path": "lib/kamal/cli/templates/secrets",
"chars": 1051,
"preview": "# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,\n# and accessori"
},
{
"path": "lib/kamal/cli.rb",
"chars": 284,
"preview": "module Kamal::Cli\n class BootError < StandardError; end\n class HookError < StandardError; end\n class LockError < Stan"
},
{
"path": "lib/kamal/commander/specifics.rb",
"chars": 1873,
"preview": "class Kamal::Commander::Specifics\n attr_reader :primary_host, :primary_role, :hosts, :roles\n delegate :stable_sort!, t"
},
{
"path": "lib/kamal/commander.rb",
"chars": 4252,
"preview": "require \"active_support/core_ext/enumerable\"\nrequire \"active_support/core_ext/module/delegation\"\nrequire \"active_support"
},
{
"path": "lib/kamal/commands/accessory/proxy.rb",
"chars": 395,
"preview": "module Kamal::Commands::Accessory::Proxy\n delegate :container_name, to: :\"config.proxy_boot\", prefix: :proxy\n\n def dep"
},
{
"path": "lib/kamal/commands/accessory.rb",
"chars": 3071,
"preview": "class Kamal::Commands::Accessory < Kamal::Commands::Base\n include Proxy\n\n attr_reader :accessory_config\n delegate :se"
},
{
"path": "lib/kamal/commands/app/assets.rb",
"chars": 1838,
"preview": "module Kamal::Commands::App::Assets\n def extract_assets\n asset_container = \"#{role.container_prefix}-assets\"\n\n co"
},
{
"path": "lib/kamal/commands/app/containers.rb",
"chars": 845,
"preview": "module Kamal::Commands::App::Containers\n DOCKER_HEALTH_LOG_FORMAT = \"'{{json .State.Health}}'\"\n\n def list_container"
},
{
"path": "lib/kamal/commands/app/error_pages.rb",
"chars": 336,
"preview": "module Kamal::Commands::App::ErrorPages\n def create_error_pages_directory\n make_directory(config.proxy_boot.error_pa"
},
{
"path": "lib/kamal/commands/app/execution.rb",
"chars": 1195,
"preview": "module Kamal::Commands::App::Execution\n def execute_in_existing_container(*command, interactive: false, env:)\n docke"
},
{
"path": "lib/kamal/commands/app/images.rb",
"chars": 289,
"preview": "module Kamal::Commands::App::Images\n def list_images\n docker :image, :ls, config.repository\n end\n\n def remove_imag"
},
{
"path": "lib/kamal/commands/app/logging.rb",
"chars": 999,
"preview": "module Kamal::Commands::App::Logging\n def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, "
},
{
"path": "lib/kamal/commands/app/proxy.rb",
"chars": 799,
"preview": "module Kamal::Commands::App::Proxy\n delegate :container_name, to: :\"config.proxy_boot\", prefix: :proxy\n\n def deploy(ta"
},
{
"path": "lib/kamal/commands/app.rb",
"chars": 3540,
"preview": "class Kamal::Commands::App < Kamal::Commands::Base\n include Assets, Containers, ErrorPages, Execution, Images, Logging,"
},
{
"path": "lib/kamal/commands/auditor.rb",
"chars": 918,
"preview": "class Kamal::Commands::Auditor < Kamal::Commands::Base\n attr_reader :details\n delegate :escape_shell_value, to: Kamal:"
},
{
"path": "lib/kamal/commands/base.rb",
"chars": 3504,
"preview": "module Kamal::Commands\n class Base\n delegate :sensitive, :argumentize, to: Kamal::Utils\n\n DOCKER_HEALTH_STATUS_FO"
},
{
"path": "lib/kamal/commands/builder/base.rb",
"chars": 3543,
"preview": "class Kamal::Commands::Builder::Base < Kamal::Commands::Base\n class BuilderError < StandardError; end\n\n ENDPOINT_DOCKE"
},
{
"path": "lib/kamal/commands/builder/clone.rb",
"chars": 879,
"preview": "module Kamal::Commands::Builder::Clone\n def clone\n git :clone, escaped_root, \"--recurse-submodules\", path: config.bu"
},
{
"path": "lib/kamal/commands/builder/cloud.rb",
"chars": 485,
"preview": "class Kamal::Commands::Builder::Cloud < Kamal::Commands::Builder::Base\n # Expects `driver` to be of format \"cloud docke"
},
{
"path": "lib/kamal/commands/builder/hybrid.rb",
"chars": 634,
"preview": "class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote\n def create\n combine \\\n create_local_"
},
{
"path": "lib/kamal/commands/builder/local.rb",
"chars": 461,
"preview": "class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base\n def create\n return if docker_driver?\n\n do"
},
{
"path": "lib/kamal/commands/builder/pack.rb",
"chars": 1296,
"preview": "class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base\n def push(export_action = \"registry\", tag_as_dirt"
},
{
"path": "lib/kamal/commands/builder/remote.rb",
"chars": 1677,
"preview": "class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base\n def create\n chain \\\n create_remote_con"
},
{
"path": "lib/kamal/commands/builder.rb",
"chars": 1061,
"preview": "require \"active_support/core_ext/string/filters\"\n\nclass Kamal::Commands::Builder < Kamal::Commands::Base\n delegate \\\n "
},
{
"path": "lib/kamal/commands/docker.rb",
"chars": 1156,
"preview": "class Kamal::Commands::Docker < Kamal::Commands::Base\n # Install Docker using the https://github.com/docker/docker-inst"
},
{
"path": "lib/kamal/commands/hook.rb",
"chars": 397,
"preview": "class Kamal::Commands::Hook < Kamal::Commands::Base\n def run(hook)\n [ hook_file(hook) ]\n end\n\n def env(secrets: fa"
},
{
"path": "lib/kamal/commands/lock.rb",
"chars": 1402,
"preview": "require \"active_support/duration\"\nrequire \"time\"\nrequire \"base64\"\n\nclass Kamal::Commands::Lock < Kamal::Commands::Base\n "
},
{
"path": "lib/kamal/commands/proxy.rb",
"chars": 4080,
"preview": "class Kamal::Commands::Proxy < Kamal::Commands::Base\n delegate :argumentize, :optionize, to: Kamal::Utils\n attr_reader"
},
{
"path": "lib/kamal/commands/prune.rb",
"chars": 1389,
"preview": "require \"active_support/duration\"\nrequire \"active_support/core_ext/numeric/time\"\n\nclass Kamal::Commands::Prune < Kamal::"
},
{
"path": "lib/kamal/commands/registry.rb",
"chars": 996,
"preview": "class Kamal::Commands::Registry < Kamal::Commands::Base\n def login(registry_config: nil)\n registry_config ||= config"
},
{
"path": "lib/kamal/commands/server.rb",
"chars": 309,
"preview": "class Kamal::Commands::Server < Kamal::Commands::Base\n def ensure_run_directory\n make_directory config.run_directory"
},
{
"path": "lib/kamal/commands.rb",
"chars": 27,
"preview": "module Kamal::Commands\nend\n"
},
{
"path": "lib/kamal/configuration/accessory.rb",
"chars": 7048,
"preview": "class Kamal::Configuration::Accessory\n include Kamal::Configuration::Validation\n\n DEFAULT_NETWORK = \"kamal\"\n\n delegat"
},
{
"path": "lib/kamal/configuration/alias.rb",
"chars": 390,
"preview": "class Kamal::Configuration::Alias\n include Kamal::Configuration::Validation\n\n attr_reader :name, :command\n\n def initi"
},
{
"path": "lib/kamal/configuration/boot.rb",
"chars": 535,
"preview": "class Kamal::Configuration::Boot\n include Kamal::Configuration::Validation\n\n attr_reader :boot_config, :host_count\n\n "
},
{
"path": "lib/kamal/configuration/builder.rb",
"chars": 4678,
"preview": "class Kamal::Configuration::Builder\n include Kamal::Configuration::Validation\n\n attr_reader :config, :builder_config\n "
},
{
"path": "lib/kamal/configuration/docs/accessory.yml",
"chars": 4569,
"preview": "# Accessories\n#\n# Accessories can be booted on a single host, a list of hosts, or on specific roles.\n# The hosts do not "
},
{
"path": "lib/kamal/configuration/docs/alias.yml",
"chars": 687,
"preview": "# Aliases\n#\n# Aliases are shortcuts for Kamal commands.\n#\n# For example, for a Rails app, you might open a console with:"
},
{
"path": "lib/kamal/configuration/docs/boot.yml",
"chars": 711,
"preview": "# Booting\n#\n# When deploying to large numbers of hosts, you might prefer not to restart your services on every host at t"
},
{
"path": "lib/kamal/configuration/docs/builder.yml",
"chars": 3603,
"preview": "# Builder\n#\n# The builder configuration controls how the application is built with `docker build`.\n#\n# See https://kamal"
},
{
"path": "lib/kamal/configuration/docs/configuration.yml",
"chars": 4782,
"preview": "# Kamal Configuration\n#\n# Configuration is read from the `config/deploy.yml`.\n\n# Destinations\n#\n# When running commands,"
},
{
"path": "lib/kamal/configuration/docs/env.yml",
"chars": 3225,
"preview": "# Environment variables\n#\n# Environment variables can be set directly in the Kamal configuration or\n# read from `.kamal/"
},
{
"path": "lib/kamal/configuration/docs/logging.yml",
"chars": 486,
"preview": "# Custom logging configuration\n#\n# Set these to control the Docker logging driver and options.\n\n# Logging settings\n#\n# T"
},
{
"path": "lib/kamal/configuration/docs/proxy.yml",
"chars": 6856,
"preview": "# Proxy\n#\n# Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to provide\n# gapless deployments. It runs "
},
{
"path": "lib/kamal/configuration/docs/registry.yml",
"chars": 2390,
"preview": "# Registry\n#\n# The default registry is Docker Hub, but you can change it using `registry/server`.\n\n# Using a local conta"
},
{
"path": "lib/kamal/configuration/docs/role.yml",
"chars": 1472,
"preview": "# Roles\n#\n# Roles are used to configure different types of servers in the deployment.\n# The most common use for this is "
},
{
"path": "lib/kamal/configuration/docs/servers.yml",
"chars": 715,
"preview": "# Servers\n#\n# Servers are split into different roles, with each role having its own configuration.\n#\n# For simpler deplo"
},
{
"path": "lib/kamal/configuration/docs/ssh.yml",
"chars": 1987,
"preview": "# SSH configuration\n#\n# Kamal uses SSH to connect and run commands on your hosts.\n# By default, it will attempt to conne"
},
{
"path": "lib/kamal/configuration/docs/sshkit.yml",
"chars": 1081,
"preview": "# SSHKit\n#\n# [SSHKit](https://github.com/capistrano/sshkit) is the SSH toolkit used by Kamal.\n#\n# The default, settings "
},
{
"path": "lib/kamal/configuration/env/tag.rb",
"chars": 275,
"preview": "class Kamal::Configuration::Env::Tag\n attr_reader :name, :config, :secrets\n\n def initialize(name, config:, secrets:)\n "
},
{
"path": "lib/kamal/configuration/env.rb",
"chars": 1136,
"preview": "class Kamal::Configuration::Env\n include Kamal::Configuration::Validation\n\n attr_reader :context, :clear, :secrets, :s"
},
{
"path": "lib/kamal/configuration/logging.rb",
"chars": 770,
"preview": "class Kamal::Configuration::Logging\n delegate :optionize, :argumentize, to: Kamal::Utils\n\n include Kamal::Configuratio"
},
{
"path": "lib/kamal/configuration/proxy/boot.rb",
"chars": 2872,
"preview": "class Kamal::Configuration::Proxy::Boot\n attr_reader :config\n delegate :argumentize, :optionize, to: Kamal::Utils\n\n d"
},
{
"path": "lib/kamal/configuration/proxy/run.rb",
"chars": 3025,
"preview": "class Kamal::Configuration::Proxy::Run\n MINIMUM_VERSION = \"v0.9.2\"\n DEFAULT_HTTP_PORT = 80\n DEFAULT_HTTPS_PORT = 443\n"
},
{
"path": "lib/kamal/configuration/proxy.rb",
"chars": 4398,
"preview": "class Kamal::Configuration::Proxy\n include Kamal::Configuration::Validation\n\n DEFAULT_LOG_REQUEST_HEADERS = [ \"Cache-C"
},
{
"path": "lib/kamal/configuration/registry.rb",
"chars": 816,
"preview": "class Kamal::Configuration::Registry\n include Kamal::Configuration::Validation\n\n def initialize(config:, secrets:, con"
},
{
"path": "lib/kamal/configuration/role.rb",
"chars": 5812,
"preview": "class Kamal::Configuration::Role\n include Kamal::Configuration::Validation\n\n delegate :argumentize, :optionize, to: Ka"
},
{
"path": "lib/kamal/configuration/servers.rb",
"chars": 596,
"preview": "class Kamal::Configuration::Servers\n include Kamal::Configuration::Validation\n\n attr_reader :config, :servers_config, "
},
{
"path": "lib/kamal/configuration/ssh.rb",
"chars": 1529,
"preview": "class Kamal::Configuration::Ssh\n LOGGER = ::Logger.new(STDERR)\n\n include Kamal::Configuration::Validation\n\n attr_read"
},
{
"path": "lib/kamal/configuration/sshkit.rb",
"chars": 506,
"preview": "class Kamal::Configuration::Sshkit\n include Kamal::Configuration::Validation\n\n attr_reader :sshkit_config\n\n def initi"
},
{
"path": "lib/kamal/configuration/validation.rb",
"chars": 763,
"preview": "require \"yaml\"\nrequire \"active_support/inflector\"\n\nmodule Kamal::Configuration::Validation\n extend ActiveSupport::Conce"
},
{
"path": "lib/kamal/configuration/validator/accessory.rb",
"chars": 381,
"preview": "class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validator\n def validate!\n super\n\n if (co"
},
{
"path": "lib/kamal/configuration/validator/alias.rb",
"chars": 446,
"preview": "class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator\n def validate!\n super\n\n name = con"
},
{
"path": "lib/kamal/configuration/validator/builder.rb",
"chars": 614,
"preview": "class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator\n def validate!\n super\n\n if confi"
},
{
"path": "lib/kamal/configuration/validator/configuration.rb",
"chars": 146,
"preview": "class Kamal::Configuration::Validator::Configuration < Kamal::Configuration::Validator\n private\n def allow_extension"
},
{
"path": "lib/kamal/configuration/validator/env.rb",
"chars": 1402,
"preview": "class Kamal::Configuration::Validator::Env < Kamal::Configuration::Validator\n SPECIAL_KEYS = [ \"clear\", \"secret\", \"tags"
},
{
"path": "lib/kamal/configuration/validator/proxy.rb",
"chars": 1529,
"preview": "class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator\n def validate!\n unless config.nil?\n "
},
{
"path": "lib/kamal/configuration/validator/registry.rb",
"chars": 875,
"preview": "class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validator\n STRING_OR_ONE_ITEM_ARRAY_KEYS = [ \"u"
},
{
"path": "lib/kamal/configuration/validator/role.rb",
"chars": 323,
"preview": "class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator\n def validate!\n validate_type! config,"
},
{
"path": "lib/kamal/configuration/validator/servers.rb",
"chars": 209,
"preview": "class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator\n def validate!\n validate_type! conf"
},
{
"path": "lib/kamal/configuration/validator.rb",
"chars": 6701,
"preview": "class Kamal::Configuration::Validator\n attr_reader :config, :example, :context\n\n def initialize(config, example:, cont"
},
{
"path": "lib/kamal/configuration/volume.rb",
"chars": 700,
"preview": "class Kamal::Configuration::Volume\n attr_reader :host_path, :container_path, :options\n delegate :argumentize, to: Kama"
},
{
"path": "lib/kamal/configuration.rb",
"chars": 12258,
"preview": "require \"active_support/ordered_options\"\nrequire \"active_support/core_ext/string/inquiry\"\nrequire \"active_support/core_e"
},
{
"path": "lib/kamal/docker.rb",
"chars": 708,
"preview": "require \"tempfile\"\nrequire \"open3\"\n\nmodule Kamal::Docker\n extend self\n BUILD_CHECK_TAG = \"kamal-local-build-check\"\n\n "
},
{
"path": "lib/kamal/env_file.rb",
"chars": 1233,
"preview": "# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.\nclass Kamal:"
},
{
"path": "lib/kamal/git.rb",
"chars": 698,
"preview": "module Kamal::Git\n extend self\n\n def used?\n system(\"git rev-parse\")\n end\n\n def user_name\n `git config user.nam"
},
{
"path": "lib/kamal/secrets/adapters/aws_secrets_manager.rb",
"chars": 1590,
"preview": "class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base\n def requires_account?\n false\n e"
},
{
"path": "lib/kamal/secrets/adapters/base.rb",
"chars": 728,
"preview": "class Kamal::Secrets::Adapters::Base\n delegate :optionize, to: Kamal::Utils\n\n def fetch(secrets, account: nil, from: n"
},
{
"path": "lib/kamal/secrets/adapters/bitwarden.rb",
"chars": 2808,
"preview": "class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base\n private\n def login(account)\n status"
},
{
"path": "lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb",
"chars": 2223,
"preview": "class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapters::Base\n def requires_account?\n fal"
},
{
"path": "lib/kamal/secrets/adapters/doppler.rb",
"chars": 1538,
"preview": "class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base\n def requires_account?\n false\n end\n\n priv"
},
{
"path": "lib/kamal/secrets/adapters/enpass.rb",
"chars": 2401,
"preview": "##\n# Enpass is different from most password managers, in a way that it's offline and doesn't need an account.\n#\n# Usage\n"
},
{
"path": "lib/kamal/secrets/adapters/gcp_secret_manager.rb",
"chars": 4420,
"preview": "class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Base\n private\n def login(account)\n "
},
{
"path": "lib/kamal/secrets/adapters/last_pass.rb",
"chars": 1199,
"preview": "class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base\n private\n def login(account)\n unless "
},
{
"path": "lib/kamal/secrets/adapters/one_password.rb",
"chars": 3297,
"preview": "class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base\n delegate :optionize, to: Kamal::Utils\n\n "
},
{
"path": "lib/kamal/secrets/adapters/passbolt.rb",
"chars": 4879,
"preview": "class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base\n def requires_account?\n false\n end\n\n pri"
},
{
"path": "lib/kamal/secrets/adapters/test.rb",
"chars": 390,
"preview": "class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base\n private\n def login(account)\n true\n en"
},
{
"path": "lib/kamal/secrets/adapters.rb",
"chars": 579,
"preview": "require \"active_support/core_ext/string/inflections\"\nmodule Kamal::Secrets::Adapters\n def self.lookup(name)\n name = "
},
{
"path": "lib/kamal/secrets/dotenv/inline_command_substitution.rb",
"chars": 1694,
"preview": "class Kamal::Secrets::Dotenv::InlineCommandSubstitution\n # Unlike dotenv, this regex does not match escaped\n # parenth"
},
{
"path": "lib/kamal/secrets.rb",
"chars": 1314,
"preview": "require \"dotenv\"\n\nclass Kamal::Secrets\n Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!\n\n def initialize(de"
},
{
"path": "lib/kamal/sshkit_with_ext.rb",
"chars": 7785,
"preview": "require \"sshkit\"\nrequire \"sshkit/dsl\"\nrequire \"net/scp\"\nrequire \"active_support/core_ext/hash/deep_merge\"\nrequire \"json\""
},
{
"path": "lib/kamal/tags.rb",
"chars": 875,
"preview": "require \"time\"\n\nclass Kamal::Tags\n attr_reader :config, :tags\n\n class << self\n def from_config(config, **extra)\n "
},
{
"path": "lib/kamal/utils/sensitive.rb",
"chars": 512,
"preview": "require \"active_support/core_ext/module/delegation\"\nrequire \"sshkit\"\n\nclass Kamal::Utils::Sensitive\n # So SSHKit knows "
},
{
"path": "lib/kamal/utils.rb",
"chars": 3346,
"preview": "require \"active_support/core_ext/object/try\"\n\nmodule Kamal::Utils\n extend self\n\n DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_R"
},
{
"path": "lib/kamal/version.rb",
"chars": 38,
"preview": "module Kamal\n VERSION = \"2.11.0\"\nend\n"
},
{
"path": "lib/kamal.rb",
"chars": 359,
"preview": "module Kamal\n class ConfigurationError < StandardError; end\nend\n\nrequire \"active_support\"\nrequire \"zeitwerk\"\nrequire \"y"
},
{
"path": "test/cli/accessory_test.rb",
"chars": 15620,
"preview": "require_relative \"cli_test_case\"\n\nclass CliAccessoryTest < CliTestCase\n setup do\n setup_test_secrets(\"secrets\" => \"M"
},
{
"path": "test/cli/app_test.rb",
"chars": 40127,
"preview": "require_relative \"cli_test_case\"\n\nclass CliAppTest < CliTestCase\n test \"boot\" do\n stub_running\n run_command(\"boot"
},
{
"path": "test/cli/build_test.rb",
"chars": 22674,
"preview": "require_relative \"cli_test_case\"\n\nclass CliBuildTest < CliTestCase\n test \"deliver\" do\n Kamal::Cli::Build.any_instanc"
},
{
"path": "test/cli/cli_test_case.rb",
"chars": 2305,
"preview": "require \"test_helper\"\n\nclass CliTestCase < ActiveSupport::TestCase\n setup do\n ENV[\"VERSION\"] = \"999\"\n "
},
{
"path": "test/cli/lock_test.rb",
"chars": 576,
"preview": "require_relative \"cli_test_case\"\n\nclass CliLockTest < CliTestCase\n test \"status\" do\n run_command(\"status\").tap do |o"
},
{
"path": "test/cli/main_test.rb",
"chars": 34659,
"preview": "require_relative \"cli_test_case\"\n\nclass CliMainTest < CliTestCase\n setup { @original_env = ENV.to_h.dup }\n teardown { "
},
{
"path": "test/cli/proxy_test.rb",
"chars": 24585,
"preview": "require_relative \"cli_test_case\"\n\nclass CliProxyTest < CliTestCase\n test \"boot\" do\n run_command(\"boot\").tap do |outp"
},
{
"path": "test/cli/prune_test.rb",
"chars": 1597,
"preview": "require_relative \"cli_test_case\"\n\nclass CliPruneTest < CliTestCase\n test \"all\" do\n Kamal::Cli::Prune.any_instance.ex"
},
{
"path": "test/cli/registry_test.rb",
"chars": 4921,
"preview": "require_relative \"cli_test_case\"\n\nclass CliRegistryTest < CliTestCase\n test \"setup\" do\n run_command(\"setup\").tap do "
},
{
"path": "test/cli/secrets_test.rb",
"chars": 1080,
"preview": "require_relative \"cli_test_case\"\n\nclass CliSecretsTest < CliTestCase\n test \"fetch\" do\n assert_equal \\\n '{\"foo\":"
},
{
"path": "test/cli/server_test.rb",
"chars": 5568,
"preview": "require_relative \"cli_test_case\"\n\nclass CliServerTest < CliTestCase\n test \"running a command with exec\" do\n SSHKit::"
},
{
"path": "test/commander_test.rb",
"chars": 6453,
"preview": "require \"test_helper\"\n\nclass CommanderTest < ActiveSupport::TestCase\n setup do\n configure_with(:deploy_with_roles)\n "
},
{
"path": "test/commands/accessory_test.rb",
"chars": 7929,
"preview": "require \"test_helper\"\n\nclass CommandsAccessoryTest < ActiveSupport::TestCase\n setup do\n setup_test_secrets(\"secrets\""
},
{
"path": "test/commands/app_test.rb",
"chars": 35894,
"preview": "require \"test_helper\"\n\nclass CommandsAppTest < ActiveSupport::TestCase\n setup do\n setup_test_secrets(\"secrets\" => \"R"
},
{
"path": "test/commands/auditor_test.rb",
"chars": 1933,
"preview": "require \"test_helper\"\nrequire \"active_support/testing/time_helpers\"\n\nclass CommandsAuditorTest < ActiveSupport::TestCase"
},
{
"path": "test/commands/builder_test.rb",
"chars": 15634,
"preview": "require \"test_helper\"\n\nclass CommandsBuilderTest < ActiveSupport::TestCase\n setup do\n @config = { service: \"app\", im"
},
{
"path": "test/commands/docker_test.rb",
"chars": 1328,
"preview": "require \"test_helper\"\n\nclass CommandsDockerTest < ActiveSupport::TestCase\n setup do\n @config = {\n service: \"app"
},
{
"path": "test/commands/hook_test.rb",
"chars": 1515,
"preview": "require \"test_helper\"\n\nclass CommandsHookTest < ActiveSupport::TestCase\n include ActiveSupport::Testing::TimeHelpers\n\n "
},
{
"path": "test/commands/lock_test.rb",
"chars": 971,
"preview": "require \"test_helper\"\n\nclass CommandsLockTest < ActiveSupport::TestCase\n setup do\n @config = {\n service: \"app\","
},
{
"path": "test/commands/proxy_test.rb",
"chars": 9695,
"preview": "require \"test_helper\"\n\nclass CommandsProxyTest < ActiveSupport::TestCase\n setup do\n @config = {\n service: \"app\""
},
{
"path": "test/commands/prune_test.rb",
"chars": 1506,
"preview": "require \"test_helper\"\n\nclass CommandsPruneTest < ActiveSupport::TestCase\n setup do\n @config = {\n service: \"app\""
},
{
"path": "test/commands/registry_test.rb",
"chars": 3241,
"preview": "require \"test_helper\"\n\nclass CommandsRegistryTest < ActiveSupport::TestCase\n setup do\n @config = {\n service: \"a"
},
{
"path": "test/commands/server_test.rb",
"chars": 543,
"preview": "require \"test_helper\"\n\nclass CommandsServerTest < ActiveSupport::TestCase\n setup do\n @config = {\n service: \"app"
},
{
"path": "test/configuration/accessory_test.rb",
"chars": 13508,
"preview": "require \"test_helper\"\n\nclass ConfigurationAccessoryTest < ActiveSupport::TestCase\n setup do\n @deploy = {\n servi"
},
{
"path": "test/configuration/boot_test.rb",
"chars": 1345,
"preview": "require \"test_helper\"\n\nclass ConfigurationBootTest < ActiveSupport::TestCase\n test \"no boot config\" do\n config = con"
},
{
"path": "test/configuration/builder_test.rb",
"chars": 5530,
"preview": "require \"test_helper\"\n\nclass ConfigurationBuilderTest < ActiveSupport::TestCase\n setup do\n @deploy = {\n service"
},
{
"path": "test/configuration/env/tags_test.rb",
"chars": 4310,
"preview": "require \"test_helper\"\n\nclass ConfigurationEnvTagsTest < ActiveSupport::TestCase\n setup do\n @deploy = {\n service"
},
{
"path": "test/configuration/env_test.rb",
"chars": 2172,
"preview": "require \"test_helper\"\n\nclass ConfigurationEnvTest < ActiveSupport::TestCase\n require \"test_helper\"\n\n test \"simple\" do\n"
},
{
"path": "test/configuration/proxy/boot_test.rb",
"chars": 1363,
"preview": "require \"test_helper\"\n\nclass ConfigurationProxyBootTest < ActiveSupport::TestCase\n setup do\n ENV[\"RAILS_MASTER_KEY\"]"
},
{
"path": "test/configuration/proxy_test.rb",
"chars": 3528,
"preview": "require \"test_helper\"\n\nclass ConfigurationProxyTest < ActiveSupport::TestCase\n setup do\n @deploy = {\n service: "
},
{
"path": "test/configuration/role_test.rb",
"chars": 11024,
"preview": "require \"test_helper\"\n\nclass ConfigurationRoleTest < ActiveSupport::TestCase\n setup do\n @deploy = {\n service: \""
},
{
"path": "test/configuration/ssh_test.rb",
"chars": 3197,
"preview": "require \"test_helper\"\n\nclass ConfigurationSshTest < ActiveSupport::TestCase\n setup do\n @deploy = {\n service: \"a"
},
{
"path": "test/configuration/sshkit_test.rb",
"chars": 1023,
"preview": "require \"test_helper\"\n\nclass ConfigurationSshkitTest < ActiveSupport::TestCase\n setup do\n @deploy = {\n service:"
},
{
"path": "test/configuration/validation_test.rb",
"chars": 7803,
"preview": "require \"test_helper\"\nclass ConfigurationValidationTest < ActiveSupport::TestCase\n test \"unknown root key\" do\n asser"
},
{
"path": "test/configuration/volume_test.rb",
"chars": 1458,
"preview": "require \"test_helper\"\n\nclass ConfigurationVolumeTest < ActiveSupport::TestCase\n test \"docker args absolute\" do\n volu"
},
{
"path": "test/configuration_test.rb",
"chars": 16613,
"preview": "require \"test_helper\"\n\nclass ConfigurationTest < ActiveSupport::TestCase\n setup do\n ENV[\"RAILS_MASTER_KEY\"] = \"456\"\n"
},
{
"path": "test/env_file_test.rb",
"chars": 1958,
"preview": "require \"test_helper\"\n\nclass EnvFileTest < ActiveSupport::TestCase\n test \"to_s\" do\n env = {\n \"foo\" => \"bar\",\n "
},
{
"path": "test/fixtures/deploy.elsewhere.yml",
"chars": 186,
"preview": "service: app3\nimage: dhh/app3\nservers:\n - \"1.1.1.3\"\n - \"1.1.1.4\"\nregistry:\n username: user\n password: pw\nbuilder:\n "
},
{
"path": "test/fixtures/deploy.erb.yml",
"chars": 222,
"preview": "service: app\nimage: dhh/app\nservers:\n - 1.1.1.1\n - 1.1.1.2\nenv:\n REDIS_URL: redis://x/y\nregistry:\n server: registry."
},
{
"path": "test/fixtures/deploy.yml",
"chars": 232,
"preview": "service: app\nimage: dhh/app\nservers:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\nregistry:\n username: user\n password: pw\nbuilder:\n ar"
},
{
"path": "test/fixtures/deploy2.yml",
"chars": 188,
"preview": "service: app2\nimage: dhh/app2\nservers:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\nregistry:\n username: user2\n password: pw2\nbuilder:\n"
},
{
"path": "test/fixtures/deploy_for_dest.mars.yml",
"chars": 63,
"preview": "servers:\n - 1.1.1.3\n - 1.1.1.4\nenv:\n REDIS_URL: redis://a/b\n"
},
{
"path": "test/fixtures/deploy_for_dest.world.yml",
"chars": 63,
"preview": "servers:\n - 1.1.1.1\n - 1.1.1.2\nenv:\n REDIS_URL: redis://x/y\n"
},
{
"path": "test/fixtures/deploy_for_dest.yml",
"chars": 159,
"preview": "service: app\nimage: dhh/app\nregistry:\n server: registry.digitalocean.com\n username: <%= \"my-user\" %>\n password: <%= \""
},
{
"path": "test/fixtures/deploy_for_required_dest.world.yml",
"chars": 63,
"preview": "servers:\n - 1.1.1.1\n - 1.1.1.2\nenv:\n REDIS_URL: redis://x/y\n"
},
{
"path": "test/fixtures/deploy_for_required_dest.yml",
"chars": 226,
"preview": "service: app\nimage: dhh/app\nregistry:\n server: registry.digitalocean.com\n username: <%= \"my-user\" %>\n password: <%= \""
},
{
"path": "test/fixtures/deploy_primary_web_role_override.yml",
"chars": 334,
"preview": "service: app\nimage: dhh/app\nservers:\n web_chicago:\n proxy: {}\n hosts:\n - 1.1.1.1\n - 1.1.1.2\n web_tokyo"
},
{
"path": "test/fixtures/deploy_simple.yml",
"chars": 130,
"preview": "service: app\nimage: dhh/app\nservers:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\nregistry:\n username: user\n password: pw\nbuilder:\n ar"
},
{
"path": "test/fixtures/deploy_with_accessories.yml",
"chars": 592,
"preview": "service: app\nimage: dhh/app\nservers:\n web:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\n workers:\n - \"1.1.1.3\"\n - \"1.1.1.4\"\nr"
},
{
"path": "test/fixtures/deploy_with_accessories_on_independent_server.yml",
"chars": 592,
"preview": "service: app\nimage: dhh/app\nservers:\n web:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\n workers:\n - \"1.1.1.3\"\n - \"1.1.1.4\"\nr"
},
{
"path": "test/fixtures/deploy_with_accessories_with_different_registries.yml",
"chars": 810,
"preview": "service: app\nimage: dhh/app\nservers:\n web:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\n workers:\n - \"1.1.1.3\"\n - \"1.1.1.4\"\nr"
},
{
"path": "test/fixtures/deploy_with_aliases.yml",
"chars": 466,
"preview": "service: app\nimage: dhh/app\nservers:\n web:\n - 1.1.1.1\n - 1.1.1.2\n workers:\n hosts:\n - 1.1.1.3\n - 1."
},
{
"path": "test/fixtures/deploy_with_assets.yml",
"chars": 157,
"preview": "service: app\nimage: dhh/app\nservers:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\nregistry:\n username: user\n password: pw\nbuilder:\n ar"
},
{
"path": "test/fixtures/deploy_with_boot_strategy.yml",
"chars": 213,
"preview": "service: app\nimage: dhh/app\nservers:\n web:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\n workers:\n - \"1.1.1.3\"\n - \"1.1.1.4\"\nb"
},
{
"path": "test/fixtures/deploy_with_cloud_builder.yml",
"chars": 692,
"preview": "service: app\nimage: dhh/app\nservers:\n web:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\n workers:\n - \"1.1.1.3\"\n - \"1.1.1.4\"\nr"
},
{
"path": "test/fixtures/deploy_with_env_tags.yml",
"chars": 493,
"preview": "service: app\nimage: dhh/app\nservers:\n web:\n - 1.1.1.1: site1\n - 1.1.1.2: [ site1 experimental ]\n - 1.2.1.1: si"
},
{
"path": "test/fixtures/deploy_with_error_pages.yml",
"chars": 155,
"preview": "service: app\nimage: dhh/app\nservers:\n - \"1.1.1.1\"\n - \"1.1.1.2\"\nregistry:\n username: user\n password: pw\nbuilder:\n ar"
}
]
// ... and 113 more files (download for full content)
About this extraction
This page contains the full source code of the basecamp/kamal GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 313 files (739.5 KB), approximately 218.9k tokens, and a symbol index with 1275 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.