Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Container create #7

Merged
merged 2 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,34 @@ A portainer like application for managing docker containers with a simple UI.
## Roadmap

### Security
- [X] Remove root access for group access method
- [X] Add authentication
- [] Remove root access for group access method
- [] Add authentication

### Containers
- [X] Can see all containers
- [X] Can see specific container
- [] Can start container TODO: Started coding start container. Need to add to index form.
- [] Can stop container
- [X] Can start container
- [X] Can stop container
- [] Can restart container
- [] Can pause container
- [] Can unpause container
- [] Can remove container
- [] Can see logs of container
- [] Can see stats of container
- [X] Can see logs of container
- [X] Can see stats of container (CPU, Memory, Network, Disk, IO) - Currently only a snapshot. See (Container Stats Monitoring)
- [] Can see exec into container
- [] Can see inspect of container
- [] Can create a container (with options)
- [X] Can create a container TODO: Need to test latest container with a fresh pull from dockerhub with noticeable change.
- [] Add UDP and TCP port mapping options.
- [] TODO: When running docker run -p 3000 it automatically creates an external port in the 5xxxx range. Should mimic the same behaviour.


### Container Stats Monitoring
- [] Set up the following as a stream
- [] Can see CPU usage
- [] Can see Memory usage
- [] Can see Network usage
- [] Can see Disk usage
- [] Can see IO usage

### Images
- [] Can see all images
Expand All @@ -48,5 +61,15 @@ Long term goal
Long term goal

## Starting

### Locally
`bundle install` to install dependencies
`rails s` to start server
`./bin/dev` to start server in build mode for tailwind

### Locally via Docker
`docker build -t azeroth_containers .` to build the docker image
`docker run -p 3000:3000 azeroth_containers` to run the docker image

### Latest build from Dockerhub (Dev)
`docker run -p 3000:3000 evilgenius13/azerothcontainers:dev`
38 changes: 29 additions & 9 deletions app/controllers/containers_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'docker'

class ContainersController < ApplicationController
include ContainersHelper
before_action :set_docker_url
before_action :set_container, only: %i[ show edit update destroy logs start stop stats ]

Expand Down Expand Up @@ -52,7 +53,8 @@ def stats

# GET /containers/new
def new
@container = Container.new
@container_form = ContainerForm.new
Rails.logger.debug "@container_form: #{@container_form.inspect}"
end

# GET /containers/1/edit
Expand All @@ -61,18 +63,36 @@ def edit

# POST /containers or /containers.json
def create
@container = Container.new(container_params)
@container_form = ContainerForm.new(container_params)

if @container_form.valid?
docker_params = create_container_params(params)

always_pull = ActiveModel::Type::Boolean.new.cast(@container_form.always_pull)
if always_pull || !image_exists_locally?(docker_params['Image'])
unless pull_image(docker_params['Image'])
flash.now[:alert] = "Failed to pull the specified image. Please check the image name and try again."
render :new, status: :unprocessable_entity and return
end
end

respond_to do |format|
if @container.save
format.html { redirect_to container_url(@container), notice: "Container was successfully created." }
format.json { render :show, status: :created, location: @container }
@docker_container = Docker::Container.create(docker_params)

if @docker_container.id.present?
StartDockerContainerJob.perform_later(@docker_container.id)
redirect_to containers_url, notice: "Container was successfully created."
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @container.errors, status: :unprocessable_entity }
# Handle the case where the Docker container does not provide an ID
flash[:alert] = "Container creation failed for an unknown reason."
render :new, status: :unprocessable_entity
end
else
# This block is reached if the form validation fails
render :new, status: :unprocessable_entity
end
end



# PATCH/PUT /containers/1 or /containers/1.json
def update
Expand Down Expand Up @@ -113,6 +133,6 @@ def set_container

# Only allow a list of trusted parameters through.
def container_params
params.require(:container).permit(:name, :status)
params.require(:container_form).permit(:name, :image, :exposed_ports, :port_bindings, :always_pull)
end
end
62 changes: 61 additions & 1 deletion app/helpers/containers_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,65 @@ def humanize_bytes(bytes)
return '0 Bytes' if bytes.to_i <= 0
i = (Math.log(bytes) / Math.log(1024)).floor
sprintf("%.2f %s", bytes.to_f / (1024 ** i), units[i])
end
end

def create_container_params(params)
container_params = params[:container_form] || {}
puts "Container Parameters: #{container_params}"

filtered_params = container_params.reject { |key, value| value.blank? }
puts "Filtered Parameters: #{filtered_params}"

allowed_keys = ['Image', 'name', 'ExposedPorts', 'PortBindings']
docker_options = {}

filtered_params.each do |key, value|
docker_key = key == 'name' ? 'name' : key.camelize

if allowed_keys.include?(docker_key)
case docker_key
when 'ExposedPorts'
# Only handling TCP
port_with_protocol = "#{value}/tcp"
docker_options['ExposedPorts'] = { port_with_protocol => {} }
when 'PortBindings'
# Only handling TCP & using filtered params port which is why it's not camelized.
internal_port_with_protocol = "#{filtered_params['exposed_ports']}/tcp"
external_port = "#{value}"
docker_options['HostConfig'] ||= {}
docker_options['HostConfig']['PortBindings'] = { internal_port_with_protocol => [{ 'HostPort' => external_port }] }
else
docker_options[docker_key] = value
end
end
end

puts "Docker Options: #{docker_options}"
docker_options
end


def image_exists_locally?(image_name)
Docker::Image.exist?(image_name)
rescue Docker::Error::DockerError => e
puts "Error checking if image exists locally: #{e.message}"
false
end

def pull_image(image_name)
Docker::Image.create('fromImage' => image_name)
puts "Image pulled successfully: #{image_name}"
true
rescue Docker::Error::DockerError => e
puts "Error pulling image: #{e.message}"
false
end

def handle_image_pull(image_name, always_pull)
if always_pull || !image_exists_locally?(image_name)
pull_image(image_name)
else
puts "Using local image: #{image_name}"
end
end
end
10 changes: 10 additions & 0 deletions app/jobs/start_docker_container_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class StartDockerContainerJob < ApplicationJob
queue_as :default

def perform(docker_container_id)
docker_container = Docker::Container.get(docker_container_id)
docker_container.start
rescue Docker::Error::DockerError => e
puts "Failed to start container: #{e.message}"
end
end
17 changes: 17 additions & 0 deletions app/models/container_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# app/forms/container_form.rb
class ContainerForm
include ActiveModel::Model

attr_accessor :image, :name, :exposed_ports, :port_bindings, :always_pull

# Validations
validates :image, presence: true
validate :name_must_be_hyphenated

private
def name_must_be_hyphenated
unless name.match?(/\A[a-z0-9]+(-[a-z0-9]+)*\z/i)
errors.add(:name, 'must contain only letters, numbers, and hyphens with no consecutive hyphens')
end
end
end
35 changes: 28 additions & 7 deletions app/views/containers/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
<%= form_with(model: container) do |form| %>
<% if container.errors.any? %>
<%= form_with(model: container_form, url: containers_path, local: true) do |form| %>
<% flash.each do |type, message| %>
<div class="<%= type %>"><%= message %></div>
<% end %>
<% if container_form.errors.any? %>
<div style="color: red">
<h2><%= pluralize(container.errors.count, "error") %> prohibited this container from being saved:</h2>
<h2><%= pluralize(container_form.errors.count, "error") %> prohibited this container from being saved:</h2>

<ul>
<% container.errors.each do |error| %>
<% container_form.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>

<div>
<%= form.label :image, style: "display: block" %>
<%= form.text_field :image %>
</div>

<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name %>
</div>

<div>
<%= form.label :status, style: "display: block" %>
<%= form.text_field :status %>
<%= form.label :container_port, style: "display: block" %>
<%= form.text_field :exposed_ports %>
</div>

<div>
<%= form.label :external_port, style: "display: block" %>
<%= form.text_field :port_bindings %>
</div>

<div>
<%= form.label :always_pull, "Always Pull Image" %><br>
<%= radio_button_tag 'container_form[always_pull]', 'true', false %>
<%= label_tag 'container_form_always_pull_true', 'On' %>
<%= radio_button_tag 'container_form[always_pull]', 'false', true %>
<%= label_tag 'container_form_always_pull_false', 'Off' %>
</div>

<div>
<%= form.submit %>
<%= form.submit "Deploy Container" %>
</div>
<% end %>
2 changes: 1 addition & 1 deletion app/views/containers/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1>New container</h1>

<%= render "form", container: @container %>
<%= render "form", container_form: @container_form %>

<br>

Expand Down
15 changes: 15 additions & 0 deletions db/migrate/20240317175410_add_docker_options_to_containers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class AddDockerOptionsToContainers < ActiveRecord::Migration[7.1]
def change
remove_column :containers, :status
add_column :containers, :image, :string
add_column :containers, :command, :string
add_column :containers, :environment_variables, :text
add_column :containers, :ports, :text
add_column :containers, :volumes, :text
add_column :containers, :cpu_limit, :string
add_column :containers, :memory_limit, :string
add_column :containers, :restart_policy, :string
add_column :containers, :network_mode, :string
add_column :containers, :labels, :text
end
end
5 changes: 5 additions & 0 deletions db/migrate/20240317175811_make_image_not_null.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class MakeImageNotNull < ActiveRecord::Migration[7.1]
def change
change_column_null :containers, :image, false
end
end
6 changes: 6 additions & 0 deletions db/migrate/20240317182009_change_port_names.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ChangePortNames < ActiveRecord::Migration[7.1]
def change
rename_column :containers, :ports, :exposed_ports
add_column :containers, :port_bindings, :text
end
end
14 changes: 12 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions test/jobs/start_docker_container_job_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "test_helper"

class StartDockerContainerJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end
Loading