baktainer/app/lib/baktainer/container.rb
James Paterni 1fa85dac55
Some checks are pending
Test and Build Docker Image / test (push) Waiting to run
Test and Build Docker Image / build (push) Blocked by required conditions
Fix container running state detection for different Docker API versions
- Handle both string and hash formats for container.info['State']
- Add fallback to container.json['State'] when needed
- Fix "Container not running" error when containers are actually running
- Support multiple Docker API response formats

The Docker API gem can return State as either:
- A simple string: "running"
- A hash: {"Status": "running", "Running": true, ...}

This fix ensures compatibility with different Docker daemon versions
and API response formats.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 08:18:46 -04:00

196 lines
5.6 KiB
Ruby

# frozen_string_literal: true
# The `Container` class represents a container abstraction within the Baktainer application.
# It is responsible for encapsulating the logic and behavior related to managing containers.
# This class serves as a core component of the application, providing methods and attributes
# to interact with and manipulate container instances.
require 'fileutils'
require 'date'
require 'baktainer/container_validator'
require 'baktainer/backup_orchestrator'
require 'baktainer/file_system_operations'
require 'baktainer/dependency_container'
class Baktainer::Container
def initialize(container, dependency_container = nil)
@container = container
@backup_command = Baktainer::BackupCommand.new
@dependency_container = dependency_container || Baktainer::DependencyContainer.new.configure
@logger = @dependency_container.get(:logger)
@file_system_ops = @dependency_container.get(:file_system_operations)
end
def id
@container.id
end
def labels
@container.info['Labels']
end
def name
container_name = @container.info['Names']&.first
container_name&.start_with?('/') ? container_name[1..-1] : container_name
end
def backup_name
labels['baktainer.name'] || name
end
def state
# Handle different Docker API response formats
state_data = @container.info['State']
if state_data.is_a?(String)
state_data
elsif state_data.is_a?(Hash)
state_data['Status']
else
# Fallback to json method if info doesn't provide expected format
@container.json['State']['Status'] rescue 'unknown'
end
end
def running?
# Check multiple ways to determine if container is running
state_data = @container.info['State']
if state_data.is_a?(String)
state_data == 'running'
elsif state_data.is_a?(Hash)
state_data['Status'] == 'running' || state_data['Running'] == true
else
# Fallback to json method
json_state = @container.json['State'] rescue nil
json_state && (json_state['Status'] == 'running' || json_state['Running'] == true)
end
end
def engine
labels['baktainer.db.engine']&.downcase
end
def login
labels['baktainer.db.user'] || nil
end
def user
login
end
def password
labels['baktainer.db.password'] || nil
end
def database
labels['baktainer.db.name'] || nil
end
def all_databases?
labels['baktainer.db.all'] == 'true'
end
def validate
validator = Baktainer::ContainerValidator.new(@container, @backup_command)
validator.validate!
true
rescue Baktainer::ValidationError => e
raise e.message
end
def backup
@logger.debug("Starting backup for container #{backup_name} with engine #{engine}.")
return unless validate
@logger.debug("Container #{backup_name} is valid for backup.")
# Create metadata for the backup orchestrator
metadata = {
name: backup_name,
engine: engine,
database: database,
user: user,
password: password,
all: all_databases?
}
orchestrator = @dependency_container.get(:backup_orchestrator)
orchestrator.perform_backup(@container, metadata)
end
def docker_container
@container
end
private
# Delegated to BackupOrchestrator
def should_compress_backup?
# Check container-specific label first
container_compress = labels['baktainer.compress']
if container_compress
return container_compress.downcase == 'true'
end
# Fall back to global environment variable (default: true)
global_compress = ENV['BT_COMPRESS']
if global_compress
return global_compress.downcase == 'true'
end
# Default to true if no setting specified
true
end
# Delegated to BackupOrchestrator and FileSystemOperations
def backup_command
if @backup_command.respond_to?(engine.to_sym)
return @backup_command.send(engine.to_sym, login: login, password: password, database: database)
elsif engine == 'custom'
return @backup_command.custom(command: labels['baktainer.command']) || raise('Custom command not defined. Set docker label bt_command.')
else
raise "Unknown engine: #{engine}"
end
end
end
# :NODOC:
class Baktainer::Containers
def self.find_all(dependency_container = nil)
dep_container = dependency_container || Baktainer::DependencyContainer.new.configure
logger = dep_container.get(:logger)
logger.debug('Searching for containers with backup labels.')
begin
containers = Docker::Container.all.select do |container|
begin
labels = container.info['Labels']
labels && labels['baktainer.backup'] == 'true'
rescue Docker::Error::DockerError => e
logger.warn("Failed to get info for container: #{e.message}")
false
end
end
logger.debug("Found #{containers.size} containers with backup labels.")
logger.debug(containers.first.class) if containers.any?
containers.map do |container|
Baktainer::Container.new(container, dep_container)
end
rescue Docker::Error::TimeoutError => e
logger.error("Docker API timeout while searching containers: #{e.message}")
raise StandardError, "Docker API timeout: #{e.message}"
rescue Docker::Error::DockerError => e
logger.error("Docker API error while searching containers: #{e.message}")
raise StandardError, "Docker API error: #{e.message}"
rescue StandardError => e
logger.error("System error while searching containers: #{e.message}")
raise StandardError, "Container search failed: #{e.message}"
end
end
end