diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82e87d6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2026 Grzegorz Luszczek + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + + PREAMBLE + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for them if you wish), that +you receive source code or can get it if you want it, that you can change the +software or use pieces of it in new free programs, and that you know you can +do these things. + +To protect your rights, we need to prevent others from denying you these +rights or asking you to surrender the rights. Therefore, you have certain +responsibilities if you distribute copies of the software, or if you modify +it: responsibilities to respect the freedom of others. + +For the full license text, see: https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f08d73 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# portainer-cli + +A Ruby command-line tool for interacting with [Portainer](https://www.portainer.io/) via its REST API. List environments, stacks, containers, volumes and networks — and open fully interactive shells inside running containers directly from your terminal. + +> **This tool was fully generated by AI** (Claude, by Anthropic) through an iterative conversation-driven development session. + +## Features + +- List endpoints, stacks, containers, volumes, and networks +- Create stacks (from file or inline compose) and containers +- **Interactive exec into containers** via WebSocket — works like `docker exec -it` but through Portainer +- Smart container name resolution: fuzzy matching using Docker Compose naming conventions (`--`) +- Automatic endpoint resolution — no need to look up endpoint IDs manually +- Supports both API key and JWT token authentication +- Configurable SSL verification (useful for self-signed certificates) +- `--run CMD` flag for running a specific command (e.g. `rails c`) +- `--quiet` flag for scripting and AI agent use cases + +## Requirements + +- Ruby 3.x +- [`websocket`](https://rubygems.org/gems/websocket) gem +- A running Portainer instance (v2.x or later) + +## Installation + +```sh +git clone https://github.com/grzlus/portainer-cli +cd portainer-cli +bundle install +chmod +x bin/portainer-cli + +# Link into your PATH (adjust the target to wherever suits you) +ln -sf "$PWD/bin/portainer-cli" ~/bin/portainer-cli +``` + +## Configuration + +```sh +portainer-cli configure +``` + +You will be prompted for: + +- **Portainer URL** — e.g. `https://portainer.example.com` +- **Authentication** — API key (recommended, no expiry) or JWT token +- **SSL verification** — answer `n` if your instance uses a self-signed certificate + +Configuration is saved to `~/.config/portainer-cli/config.yml` (mode `0600`). + +### Manual config + +```yaml +# ~/.config/portainer-cli/config.yml +url: https://portainer.example.com +api_key: ptr_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +ssl_verify: true +``` + +## Usage + +### List resources + +```sh +portainer-cli list endpoints +portainer-cli list stacks +portainer-cli list containers +portainer-cli list volumes +portainer-cli list networks +``` + +### Open an interactive shell + +```sh +# Resolves endpoint automatically from the stack name. +# Container name defaults to the stack name, then falls back to 'app'. +portainer-cli console +portainer-cli console + +# Examples +portainer-cli console myapp +portainer-cli console myapp worker +portainer-cli console myapp --shell /bin/bash +``` + +### Run a specific command + +```sh +portainer-cli console myapp --run "rails c" +portainer-cli console myapp --run "bundle exec rake db:migrate" +``` + +### Quiet mode (scripting / AI agents) + +Suppresses all status output — only the container's own stdout/stderr is printed. + +```sh +portainer-cli console myapp --quiet --run "rails runner 'puts User.count'" +``` + +### Create resources + +```sh +# Stack from a compose file +portainer-cli create stack --endpoint 1 --name myapp --file docker-compose.yml + +# Stack from inline compose +portainer-cli create stack --endpoint 1 --name myapp --compose "version: '3' +services: + web: + image: nginx" + +# Container +portainer-cli create container --endpoint 1 --image nginx:alpine --name web \ + --port 8080:80 --env APP_ENV=production +``` + +### Low-level exec (manual endpoint ID) + +```sh +portainer-cli exec +portainer-cli exec --shell /bin/bash +``` + +## Container name resolution + +When using `console`, names are resolved in this order: + +1. **Exact match** — full container name or ID prefix +2. **Compose pattern** — `--` (e.g. `librarian` → `librarian-librarian-1`) +3. **Substring match** — scoped to the stack's own containers +4. **`app` fallback** — tries `-app-1` as a last resort + +If multiple containers match ambiguously, a list is printed and the command exits. + +## Debugging + +```sh +portainer-cli --debug console myapp +``` + +Prints the WebSocket URL, auth headers, and the full HTTP upgrade handshake request/response — useful for diagnosing connection issues behind reverse proxies. + +## License + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +See [LICENSE](LICENSE) for the full text. diff --git a/lib/portainer_cli/config.rb b/lib/portainer_cli/config.rb index 7714b42..8c799f0 100644 --- a/lib/portainer_cli/config.rb +++ b/lib/portainer_cli/config.rb @@ -5,7 +5,7 @@ require 'fileutils' module PortainerCli class Config - CONFIG_DIR = File.expand_path('~/.portainer-cli').freeze + CONFIG_DIR = File.expand_path('~/.config/portainer-cli').freeze CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml').freeze attr_accessor :url, :token, :api_key, :ssl_verify