Initial implementation of portainer-cli

Ruby CLI tool for interacting with the Portainer API. Supports listing
endpoints/stacks/containers/volumes/networks, creating stacks and containers,
and opening an interactive exec session in a container via WebSocket.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 12:58:33 +01:00
commit d3cb4fdebd
14 changed files with 815 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
# frozen_string_literal: true
require 'net/http'
require 'json'
require 'uri'
require 'openssl'
module PortainerCli
class Client
class ApiError < StandardError
attr_reader :code
def initialize(msg, code)
super(msg)
@code = code
end
end
def initialize(config)
@config = config
@base = URI.parse(config.url.chomp('/'))
end
def get(path, params = {})
request(:get, path, params: params)
end
def post(path, body = nil)
request(:post, path, body: body)
end
def delete(path)
request(:delete, path)
end
# Returns the raw websocket URL (ws:// or wss://) for exec sessions
def websocket_url(path, params = {})
uri = ws_uri(path)
uri.query = URI.encode_www_form(params) unless params.empty?
uri.to_s
end
def auth_header
@config.auth_header
end
private
def request(method, path, params: {}, body: nil)
uri = http_uri(path)
uri.query = URI.encode_www_form(params) unless params.empty?
http = build_http(uri)
req = build_request(method, uri, body)
resp = http.request(req)
handle_response(resp)
end
def build_http(uri)
http = Net::HTTP.new(uri.host, uri.port)
if uri.scheme == 'https'
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
end
http.open_timeout = 10
http.read_timeout = 30
http
end
def build_request(method, uri, body)
klass = {
get: Net::HTTP::Get,
post: Net::HTTP::Post,
delete: Net::HTTP::Delete
}.fetch(method)
req = klass.new(uri)
req['Content-Type'] = 'application/json'
req['Accept'] = 'application/json'
@config.auth_header.each { |k, v| req[k] = v }
req.body = JSON.generate(body) if body
req
end
def handle_response(resp)
code = resp.code.to_i
raise ApiError.new("HTTP #{code}: #{resp.body}", code) if code >= 400
return nil if resp.body.nil? || resp.body.empty?
JSON.parse(resp.body)
rescue JSON::ParserError
resp.body
end
def http_uri(path)
URI.parse("#{@base}#{path}")
end
def ws_uri(path)
scheme = @base.scheme == 'https' ? 'wss' : 'ws'
URI.parse("#{scheme}://#{@base.host}:#{@base.port}#{path}")
end
end
end