mirror of
https://github.com/czhu12/canine.git
synced 2025-12-17 00:44:33 -06:00
resource constraints and services refactor
This commit is contained in:
@@ -7,7 +7,6 @@ class AddOns::InstallHelmChart
|
||||
|
||||
add_on.update_install_stage!(0)
|
||||
create_namespace(context.connection)
|
||||
apply_resource_quota(context.connection)
|
||||
|
||||
if add_on.installed?
|
||||
add_on.updating!
|
||||
@@ -65,16 +64,6 @@ class AddOns::InstallHelmChart
|
||||
kubectl.apply_yaml(namespace_yaml)
|
||||
end
|
||||
|
||||
def self.apply_resource_quota(connection)
|
||||
add_on = connection.add_on
|
||||
return unless add_on.resource_constraint.present?
|
||||
|
||||
runner = Cli::RunAndLog.new(add_on)
|
||||
kubectl = K8::Kubectl.new(connection, runner)
|
||||
resource_quota_yaml = K8::Stateless::ResourceQuota.new(add_on).to_yaml
|
||||
kubectl.apply_yaml(resource_quota_yaml)
|
||||
end
|
||||
|
||||
def self.get_values(add_on)
|
||||
# Merge the values from the form with the values.yaml object and create a new values.yaml file
|
||||
values = add_on.values
|
||||
|
||||
@@ -7,6 +7,11 @@ class ResourceConstraints::Save
|
||||
# Get params hash
|
||||
rc_params = context.params
|
||||
|
||||
# Convert blank strings to nil
|
||||
rc_params.each do |key, value|
|
||||
rc_params[key] = nil if value.blank?
|
||||
end
|
||||
|
||||
# Convert CPU cores to millicores
|
||||
if rc_params[:cpu_request].present?
|
||||
rc_params[:cpu_request] = (rc_params[:cpu_request].to_f * 1000).to_i
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
class Projects::Services::ResourceConstraintsController < Projects::Services::BaseController
|
||||
before_action :set_service
|
||||
|
||||
def create
|
||||
result = ResourceConstraints::Create.call(@service.build_resource_constraint, resource_constraint_params)
|
||||
|
||||
if result.success?
|
||||
@service.updated!
|
||||
render_partial
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
render_partial
|
||||
end
|
||||
|
||||
def new
|
||||
render_partial(show_form: true)
|
||||
end
|
||||
|
||||
def update
|
||||
result = ResourceConstraints::Update.call(@service.resource_constraint, resource_constraint_params)
|
||||
@service.updated!
|
||||
|
||||
if result.success?
|
||||
render_partial
|
||||
else
|
||||
raise StandardError, result.message
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@service.resource_constraint.destroy
|
||||
@service.updated!
|
||||
render_partial
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_partial(locals = {})
|
||||
render partial: "projects/services/resource_constraints/show", locals: { service: @service, resource_constraint: @service.resource_constraint }.merge(locals)
|
||||
end
|
||||
|
||||
def resource_constraint_params
|
||||
params.require(:resource_constraint).permit(
|
||||
:cpu_request,
|
||||
:cpu_limit,
|
||||
:memory_request,
|
||||
:memory_limit,
|
||||
:gpu_request
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
class ResourceConstraintsController < ApplicationController
|
||||
before_action :set_constrainable
|
||||
before_action :set_resource_constraint, only: [ :edit, :update, :destroy ]
|
||||
|
||||
def new
|
||||
@resource_constraint = @constrainable.build_resource_constraint
|
||||
end
|
||||
|
||||
def create
|
||||
@resource_constraint = @constrainable.build_resource_constraint
|
||||
result = ResourceConstraints::Create.call(@resource_constraint, resource_constraint_params)
|
||||
|
||||
if result.success?
|
||||
redirect_to_constrainable notice: "Resource constraints created successfully."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
result = ResourceConstraints::Update.call(@resource_constraint, resource_constraint_params)
|
||||
|
||||
if result.success?
|
||||
redirect_to_constrainable notice: "Resource constraints updated successfully."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@resource_constraint.destroy
|
||||
redirect_to_constrainable notice: "Resource constraints removed."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_constrainable
|
||||
# Determine the constrainable type and ID from params
|
||||
if params[:add_on_id]
|
||||
@constrainable = current_account.add_ons.find(params[:add_on_id])
|
||||
@constrainable_type = 'add_on'
|
||||
elsif params[:project_id]
|
||||
@constrainable = current_account.projects.find(params[:project_id])
|
||||
@constrainable_type = 'project'
|
||||
elsif params[:service_id]
|
||||
# Services are nested under projects
|
||||
project = current_account.projects.find(params[:project_id]) if params[:project_id]
|
||||
@constrainable = project.services.find(params[:service_id])
|
||||
@constrainable_type = 'service'
|
||||
@project = project
|
||||
else
|
||||
redirect_to root_path, alert: "Invalid resource"
|
||||
end
|
||||
end
|
||||
|
||||
def set_resource_constraint
|
||||
@resource_constraint = @constrainable.resource_constraint
|
||||
redirect_to_constrainable(alert: "No resource constraints found.") unless @resource_constraint
|
||||
end
|
||||
|
||||
def resource_constraint_params
|
||||
params.require(:resource_constraint).permit(
|
||||
:cpu_request,
|
||||
:cpu_limit,
|
||||
:memory_request,
|
||||
:memory_limit,
|
||||
:gpu_request
|
||||
)
|
||||
end
|
||||
|
||||
def redirect_to_constrainable(options = {})
|
||||
case @constrainable_type
|
||||
when 'add_on'
|
||||
redirect_to edit_add_on_path(@constrainable), **options
|
||||
when 'project'
|
||||
redirect_to edit_project_path(@constrainable), **options
|
||||
when 'service'
|
||||
redirect_to project_services_path(@project), **options
|
||||
else
|
||||
redirect_to root_path, **options
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
module ServicesHelper
|
||||
def services_layout(service, tab, &block)
|
||||
render layout: 'services/layout', locals: { service:, tab: }, &block
|
||||
render layout: 'projects/services/layout', locals: { service:, tab: }, &block
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,7 +34,9 @@ module StorageHelper
|
||||
SIZE_UNITS.to_a.reverse.each do |unit, bytes|
|
||||
if integer >= bytes
|
||||
value = (integer.to_f / bytes).round(2)
|
||||
return "#{value}#{unit}i"
|
||||
# Remove unnecessary trailing zeros and decimal point
|
||||
formatted_value = value % 1 == 0 ? value.to_i : value
|
||||
return "#{formatted_value}#{unit}i"
|
||||
end
|
||||
end
|
||||
integer.to_s
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["container"]
|
||||
static targets = ["container", "input", "slider", "cpu_requestField", "cpu_limitField", "memory_requestField", "memory_limitField"]
|
||||
|
||||
toggleResourceConstraints(event) {
|
||||
if (event.target.checked) {
|
||||
@@ -10,4 +10,37 @@ export default class extends Controller {
|
||||
this.containerTarget.classList.add("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
toggleField(event) {
|
||||
const fieldName = event.params.field
|
||||
const isEnabled = event.target.checked
|
||||
const fieldTarget = `${fieldName}FieldTarget`
|
||||
|
||||
if (this[fieldTarget]) {
|
||||
const field = this[fieldTarget]
|
||||
const numberInput = field.querySelector('input[type="number"]')
|
||||
const rangeInput = field.querySelector('input[type="range"]')
|
||||
|
||||
if (isEnabled) {
|
||||
// Enable and set default values
|
||||
numberInput.removeAttribute('readonly')
|
||||
numberInput.classList.remove('opacity-50', 'cursor-not-allowed')
|
||||
rangeInput.disabled = false
|
||||
|
||||
if (fieldName.includes('cpu')) {
|
||||
numberInput.value = '0.5'
|
||||
rangeInput.value = '0.5'
|
||||
} else if (fieldName.includes('memory')) {
|
||||
numberInput.value = '128'
|
||||
rangeInput.value = '128'
|
||||
}
|
||||
} else {
|
||||
// Make readonly and set to empty so form sends nil
|
||||
numberInput.setAttribute('readonly', 'readonly')
|
||||
numberInput.classList.add('opacity-50', 'cursor-not-allowed')
|
||||
rangeInput.disabled = true
|
||||
numberInput.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
app/javascript/controllers/turbo_tabs_controller.js
Normal file
21
app/javascript/controllers/turbo_tabs_controller.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["tabs", "content"]
|
||||
|
||||
connect() {
|
||||
console.log(this.contentTarget)
|
||||
this.tabsTarget.addEventListener("click", (event) => {
|
||||
event.preventDefault()
|
||||
this.tabsTarget.querySelectorAll(".tab").forEach((radio) => {
|
||||
radio.classList.remove("tab-active")
|
||||
})
|
||||
event.target.classList.add("tab-active")
|
||||
// Show loading spinner
|
||||
this.contentTarget.innerHTML = `<div class="flex items-center justify-center my-6">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
</div>`
|
||||
this.contentTarget.src = event.target.href
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ require "base64"
|
||||
require "json"
|
||||
|
||||
class Projects::DeploymentJob < ApplicationJob
|
||||
DEPLOYABLE_RESOURCES = %w[ConfigMap ResourceQuota Deployment CronJob Service Ingress Pv Pvc]
|
||||
DEPLOYABLE_RESOURCES = %w[ConfigMap Deployment CronJob Service Ingress Pv Pvc]
|
||||
class DeploymentFailure < StandardError; end
|
||||
|
||||
def perform(deployment, user)
|
||||
@@ -22,7 +22,6 @@ class Projects::DeploymentJob < ApplicationJob
|
||||
upload_registry_secrets(kubectl, deployment)
|
||||
apply_config_map(project, kubectl)
|
||||
|
||||
deploy_resource_quotas(project, kubectl)
|
||||
deploy_volumes(project, kubectl)
|
||||
predeploy(project, kubectl, connection)
|
||||
# For each of the projects services
|
||||
@@ -44,12 +43,6 @@ class Projects::DeploymentJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def deploy_resource_quotas(project, kubectl)
|
||||
if project.resource_constraint.present?
|
||||
apply_resource_quota(project, kubectl)
|
||||
end
|
||||
end
|
||||
|
||||
def deploy_volumes(project, kubectl)
|
||||
project.volumes.each do |volume|
|
||||
begin
|
||||
|
||||
@@ -26,7 +26,6 @@ class AddOn < ApplicationRecord
|
||||
include Loggable
|
||||
belongs_to :cluster
|
||||
has_one :account, through: :cluster
|
||||
has_one :resource_constraint, as: :constrainable, dependent: :destroy
|
||||
|
||||
enum :status, {
|
||||
installing: 0,
|
||||
|
||||
@@ -47,7 +47,6 @@ class Project < ApplicationRecord
|
||||
|
||||
has_one :project_credential_provider, dependent: :destroy
|
||||
has_one :build_configuration, dependent: :destroy
|
||||
has_one :resource_constraint, as: :constrainable, dependent: :destroy
|
||||
|
||||
has_one :child_fork, class_name: "ProjectFork", foreign_key: :child_project_id, dependent: :destroy
|
||||
has_many :forks, class_name: "ProjectFork", foreign_key: :parent_project_id, dependent: :destroy
|
||||
|
||||
@@ -2,25 +2,24 @@
|
||||
#
|
||||
# Table name: resource_constraints
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# constrainable_type :string not null
|
||||
# cpu_limit :bigint
|
||||
# cpu_request :bigint
|
||||
# gpu_request :integer
|
||||
# memory_limit :bigint
|
||||
# memory_request :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# constrainable_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# cpu_limit :bigint
|
||||
# cpu_request :bigint
|
||||
# gpu_request :integer
|
||||
# memory_limit :bigint
|
||||
# memory_request :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# service_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_resource_constraints_on_constrainable (constrainable_type,constrainable_id)
|
||||
# index_resource_constraints_on_service_id (service_id)
|
||||
#
|
||||
class ResourceConstraint < ApplicationRecord
|
||||
include StorageHelper
|
||||
|
||||
belongs_to :constrainable, polymorphic: true
|
||||
belongs_to :service
|
||||
|
||||
validates :cpu_request, numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
|
||||
validates :cpu_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
|
||||
@@ -28,5 +27,24 @@ class ResourceConstraint < ApplicationRecord
|
||||
validates :memory_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
|
||||
validates :gpu_request, numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
|
||||
|
||||
# Convert string input to integers (e.g., "500m" -> 500, "1Gi" -> 1073741824)
|
||||
# Formatted getters for Kubernetes YAML templates
|
||||
def cpu_request_formatted
|
||||
return nil if cpu_request.nil?
|
||||
integer_to_compute(cpu_request)
|
||||
end
|
||||
|
||||
def cpu_limit_formatted
|
||||
return nil if cpu_limit.nil?
|
||||
integer_to_compute(cpu_limit)
|
||||
end
|
||||
|
||||
def memory_request_formatted
|
||||
return nil if memory_request.nil?
|
||||
integer_to_memory(memory_request)
|
||||
end
|
||||
|
||||
def memory_limit_formatted
|
||||
return nil if memory_limit.nil?
|
||||
integer_to_memory(memory_limit)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,7 +42,7 @@ class Service < ApplicationRecord
|
||||
scope :running, -> { where(status: [ :healthy, :unhealthy, :updated ]) }
|
||||
|
||||
has_one :cron_schedule, dependent: :destroy
|
||||
has_one :resource_constraint, as: :constrainable, dependent: :destroy
|
||||
has_one :resource_constraint, dependent: :destroy
|
||||
|
||||
validates :cron_schedule, presence: true, if: :cron_job?
|
||||
validates :command, presence: true, if: :cron_job?
|
||||
|
||||
@@ -25,7 +25,8 @@ class K8::Base
|
||||
def to_yaml
|
||||
template_content = template_path.read
|
||||
erb_template = ERB.new(template_content)
|
||||
erb_template.result(binding)
|
||||
result = erb_template.result(binding)
|
||||
result.gsub(/\n\s*\n/, "\n")
|
||||
end
|
||||
|
||||
def client
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
</div>
|
||||
<div>
|
||||
<!-- link to github -->
|
||||
<%= link_to @build.commit_sha[0..6], "https://github.com/#{@project.repository_url}/commit/#{@build.commit_sha}", class: "underline", target: "_blank", rel: "noopener noreferrer" %>
|
||||
<span class="font-light"><%= @build.commit_message.truncate(50) %></span>
|
||||
<%= link_to(
|
||||
@build.commit_sha[0..6],
|
||||
"https://github.com/#{@project.repository_url}/commit/#{@build.commit_sha}",
|
||||
class: "underline",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer"
|
||||
) %>
|
||||
<span class="font-light"><%= @build.commit_message.truncate(75) %></span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-2 mb-6 border-base-content/10" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= services_layout(service, tab) do %>
|
||||
<div>
|
||||
<h1>Advanced</h1>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Resource Constraints</h2>
|
||||
<hr class="mt-3 mb-4 border-t border-base-300" />
|
||||
<%= render "projects/services/resource_constraints/show", service: @service, resource_constraint: @service.resource_constraint %>
|
||||
</div>
|
||||
10
app/views/projects/services/_cron_job.html.erb
Normal file
10
app/views/projects/services/_cron_job.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<%= render "projects/services/cron_job_history", service: service %>
|
||||
</div>
|
||||
<div class="ml-4" data-tip="Run Job" class="tooltip">
|
||||
<%= button_to "Run Job", project_service_jobs_path(service.project, service), class: "btn btn-primary btn-sm btn-outline #{!service.healthy? ? 'btn-disabled' : ''}", disabled: !service.healthy? %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,21 @@
|
||||
<%= services_layout(service, tab) do %>
|
||||
<div>
|
||||
<h1>Networking <%= service.name %></h1>
|
||||
<div>
|
||||
<div class="mb-6 space-y-2">
|
||||
<h2 class="text-xl font-bold">Internal URL</h2>
|
||||
<hr class="mt-3 mb-4 border-t border-base-300" />
|
||||
<pre
|
||||
class="inline-block cursor-pointer"
|
||||
data-controller="clipboard"
|
||||
data-clipboard-text="<%= service.internal_url %>"><%= service.internal_url %></pre>
|
||||
|
||||
<%= render "projects/services/telepresence_guide", cluster: @project.cluster, url: "http://#{service.internal_url}" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Public Networking</h2>
|
||||
<hr class="mt-3 mb-4 border-t border-base-300" />
|
||||
<% if service.allow_public_networking? %>
|
||||
<div class="form-control form-group">
|
||||
<%= render "projects/services/domains/index", service: service %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,97 +1,63 @@
|
||||
<div>
|
||||
<%= services_layout(service, tab) do %>
|
||||
<% if service.web_service? %>
|
||||
<div class="mb-4 space-y-2">
|
||||
<h6 class="text-lg font-bold">Internal URL</h6>
|
||||
<pre
|
||||
class="inline-block cursor-pointer"
|
||||
data-controller="clipboard"
|
||||
data-clipboard-text="<%= service.internal_url %>"><%= service.internal_url %></pre>
|
||||
|
||||
<%= render "projects/services/telepresence_guide", cluster: @project.cluster, url: "http://#{service.internal_url}" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if service.cron_job? %>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<%= render "projects/services/cron_job_history", service: service %>
|
||||
</div>
|
||||
<div class="ml-4" data-tip="Run Job" class="tooltip">
|
||||
<%= button_to "Run Job", project_service_jobs_path(service.project, service), class: "btn btn-primary btn-sm btn-outline #{!service.healthy? ? 'btn-disabled' : ''}", disabled: !service.healthy? %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_with(model: [service.project, service]) do |form| %>
|
||||
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2">
|
||||
<div>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, class: "input input-bordered w-full", required: true, disabled: true %>
|
||||
</div>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :command %>
|
||||
<%= form.text_field :command, class: "input input-bordered w-full", required: false %>
|
||||
</div>
|
||||
<% if service.cron_job? %>
|
||||
<div class="form-control form-group">
|
||||
<%= form.fields_for :cron_schedule do |cron_schedule_form| %>
|
||||
<%= cron_schedule_form.label :schedule %>
|
||||
<%= cron_schedule_form.text_field :schedule, class: "input input-bordered w-full", placeholder: "0 0 * * *", value: service.cron_schedule.schedule %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if service.web_service? %>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :container_port %>
|
||||
<%= form.text_field :container_port, class: "input input-bordered w-full", required: false %>
|
||||
</div>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :healthcheck_url %>
|
||||
<%= form.text_field :healthcheck_url, class: "input input-bordered w-full", placeholder: "/health" %>
|
||||
<span class="label-text-alt">Optional: The endpoint just needs to return a 200 status code to be considered healthy</span>
|
||||
</div>
|
||||
<div class="form-control rounded-lg bg-base-200 p-2 px-4">
|
||||
<label class="label mt-1">
|
||||
<span class="label-text cursor-pointer">Allow public networking</span>
|
||||
<%= form.check_box :allow_public_networking, class: "checkbox" %>
|
||||
</label>
|
||||
</div>
|
||||
<span class="label-text-alt">Checking this allows your service to be accessible from the public internet</span>
|
||||
<% end %>
|
||||
<% if service.web_service? || service.background_service? %>
|
||||
<div>
|
||||
<h2 class="text-lg my-2 mt-4">Resources</h2>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :replicas %>
|
||||
<%= form.number_field :replicas, class: "input input-bordered w-full max-w-xs", placeholder: "1" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description %>
|
||||
<%= render "shared/partials/markdown_editor", form: form %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= button_to [service.project, service], method: :delete, class: "btn btn-error btn-outline mt-2", form: { data: { turbo_confirm: t("are_you_sure") } } do %>
|
||||
<iconify-icon icon="lucide:trash" height="20" class="text-error-content"></iconify-icon>
|
||||
Delete service
|
||||
<% end %>
|
||||
<% if service.web_service? && service.allow_public_networking? %>
|
||||
<div class="my-8">
|
||||
<h2 class="text-2xl font-bold">Networking</h2>
|
||||
<hr class="mt-3 mb-4 border-t border-base-300" />
|
||||
|
||||
<%= form_with(model: [service.project, service]) do |form| %>
|
||||
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2">
|
||||
<div>
|
||||
<div class="form-control form-group">
|
||||
<%= render "projects/services/domains/index", service: service %>
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, class: "input input-bordered w-full", required: true, disabled: true %>
|
||||
</div>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :command %>
|
||||
<%= form.text_field :command, class: "input input-bordered w-full", required: false %>
|
||||
</div>
|
||||
<% if service.cron_job? %>
|
||||
<div class="form-control form-group">
|
||||
<%= form.fields_for :cron_schedule do |cron_schedule_form| %>
|
||||
<%= cron_schedule_form.label :schedule %>
|
||||
<%= cron_schedule_form.text_field :schedule, class: "input input-bordered w-full", placeholder: "0 0 * * *", value: service.cron_schedule.schedule %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if service.web_service? %>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :container_port %>
|
||||
<%= form.text_field :container_port, class: "input input-bordered w-full", required: false %>
|
||||
</div>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :healthcheck_url %>
|
||||
<%= form.text_field :healthcheck_url, class: "input input-bordered w-full", placeholder: "/health" %>
|
||||
<span class="label-text-alt">Optional: The endpoint just needs to return a 200 status code to be considered healthy</span>
|
||||
</div>
|
||||
<div class="form-control rounded-lg bg-base-200 p-2 px-4">
|
||||
<label class="label mt-1">
|
||||
<span class="label-text cursor-pointer">Allow public networking</span>
|
||||
<%= form.check_box :allow_public_networking, class: "checkbox" %>
|
||||
</label>
|
||||
</div>
|
||||
<span class="label-text-alt">Checking this allows your service to be accessible from the public internet</span>
|
||||
<% end %>
|
||||
<% if service.web_service? || service.background_service? %>
|
||||
<div>
|
||||
<h2 class="text-lg my-2 mt-4">Resources</h2>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :replicas %>
|
||||
<%= form.number_field :replicas, class: "input input-bordered w-full max-w-xs", placeholder: "1" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<%= form.label :description %>
|
||||
<%= render "shared/partials/markdown_editor", form: form %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= button_to [service.project, service], method: :delete, class: "btn btn-error btn-outline mt-2", form: { data: { turbo_confirm: t("are_you_sure") } } do %>
|
||||
<iconify-icon icon="lucide:trash" height="20" class="text-error-content"></iconify-icon>
|
||||
Delete service
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,11 +1,13 @@
|
||||
<div>
|
||||
<%= turbo_frame_tag "service_#{service.id}" do %>
|
||||
<%= turbo_frame_tag("service_#{service.id}", data: { turbo_tabs_target: "content" }) do %>
|
||||
<div class="my-6">
|
||||
<% if tab == "overview" %>
|
||||
<%= render "projects/services/overview", service: service, tab: tab %>
|
||||
<%= render "projects/services/overview", service:, tab: %>
|
||||
<% elsif tab == "cron-jobs" %>
|
||||
<%= render "projects/services/cron_job", service:, tab: %>
|
||||
<% elsif tab == "networking" %>
|
||||
<%= render "projects/services/networking", service: service, tab: tab %>
|
||||
<%= render "projects/services/networking", service:, tab: %>
|
||||
<% elsif tab == "advanced" %>
|
||||
<%= render "projects/services/advanced", service: service, tab: tab %>
|
||||
<%= render "projects/services/advanced", service:, tab: %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
13
app/views/projects/services/_tabs.html.erb
Normal file
13
app/views/projects/services/_tabs.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<div role="tablist" class="tabs tabs-bordered" data-turbo-tabs-target="tabs">
|
||||
<%= link_to "Overview", project_service_path(service.project, service, tab: 'overview'), class: "tab #{'tab-active' if tab == 'overview'}" %>
|
||||
|
||||
<% if service.web_service? %>
|
||||
<%= link_to "Networking", project_service_path(service.project, service, tab: 'networking'), class: "tab #{'tab-active' if tab == 'networking'}" %>
|
||||
<% end %>
|
||||
|
||||
<% if service.cron_job? %>
|
||||
<%= link_to "Cron Job History", project_service_path(service.project, service, tab: 'cron-jobs'), class: "tab #{'tab-active' if tab == 'cron-jobs'}" %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to "Advanced", project_service_path(service.project, service, tab: 'advanced'), class: "tab #{'tab-active' if tab == 'advanced'}" %>
|
||||
</div>
|
||||
@@ -21,16 +21,14 @@
|
||||
<div class="collapse-title">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-xl font-medium">
|
||||
<div>
|
||||
<% if service.web_service? %>
|
||||
<iconify-icon icon="ph:globe-duotone" height="16"></iconify-icon>
|
||||
<% elsif service.background_service? %>
|
||||
<iconify-icon icon="ph:server" height="16"></iconify-icon>
|
||||
<% elsif service.cron_job? %>
|
||||
<iconify-icon icon="ph:clock" height="16"></iconify-icon>
|
||||
<% end %>
|
||||
<%= service.name %>
|
||||
</div>
|
||||
<% if service.web_service? %>
|
||||
<iconify-icon icon="lucide:globe" height="16"></iconify-icon>
|
||||
<% elsif service.background_service? %>
|
||||
<iconify-icon icon="lucide:cpu" height="16"></iconify-icon>
|
||||
<% elsif service.cron_job? %>
|
||||
<iconify-icon icon="lucide:clock" height="16"></iconify-icon>
|
||||
<% end %>
|
||||
<%= service.name %>
|
||||
</div>
|
||||
<div class="my-1">
|
||||
<%= render "projects/services/status", service: service %>
|
||||
@@ -38,7 +36,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<%= render "projects/services/show", service: service, tab: "overview" %>
|
||||
<div data-controller="turbo-tabs">
|
||||
<%= render "projects/services/tabs", service:, tab: "overview" %>
|
||||
<%= render "projects/services/show", service:, tab: "overview" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<div data-controller="resource-constraints--slider" data-resource-constraints--slider-type-value="float" data-resource-constraints--form-target="<%= key %>Field">
|
||||
<% value = resource_constraint.send(key) %>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<%= check_box_tag "enable_#{key}", "1", value.present?,
|
||||
class: "checkbox checkbox-sm",
|
||||
data: {
|
||||
action: "change->resource-constraints--form#toggleField",
|
||||
resource_constraints__form_field_param: key
|
||||
} %>
|
||||
<span class="text-sm"><%= key.to_s.titleize %></span>
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<%= form.text_field(
|
||||
key,
|
||||
type: "number",
|
||||
class: "input input-sm input-bordered font-mono text-sm w-[125px] text-right mr-2 #{'opacity-50 cursor-not-allowed' if value.blank?}",
|
||||
value: value.present? ? (value / 1000.0) : '',
|
||||
step: :any,
|
||||
required: false,
|
||||
placeholder: "0.5",
|
||||
readonly: value.blank?,
|
||||
data: {
|
||||
resource_constraints__slider_target: "numberInput",
|
||||
resource_constraints__form_target: "input",
|
||||
action: "input->resource-constraints--slider#updateSlider"
|
||||
}
|
||||
) %>
|
||||
<span class="text-gray-200">cores</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="16"
|
||||
step="0.1"
|
||||
value="<%= value.present? ? (value / 1000.0) : 0.5 %>"
|
||||
class="w-full h-2 bg-base-300 rounded-lg appearance-none cursor-pointer slider"
|
||||
disabled="<%= value.blank? %>"
|
||||
data-action="input->resource-constraints--slider#updateValue"
|
||||
data-resource-constraints--slider-target="slider"
|
||||
data-resource-constraints--form-target="slider"
|
||||
>
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>0.1</span>
|
||||
<span>16.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="space-y-6" data-controller="resource-constraints--form">
|
||||
<div data-resource-constraints--form-target="container">
|
||||
<!-- CPU Configuration -->
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-16 md:gap-32">
|
||||
<div>
|
||||
<div class="font-medium flex items-center gap-2 font-bold mb-4">
|
||||
<iconify-icon icon="lucide:cpu" height="20"></iconify-icon>
|
||||
<span>CPU</span>
|
||||
</div>
|
||||
|
||||
<%= render "projects/services/resource_constraints/cpu_slider", form: form, resource_constraint:, key: :cpu_request %>
|
||||
<%= render "projects/services/resource_constraints/cpu_slider", form: form, resource_constraint:, key: :cpu_limit %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium flex items-center gap-2 font-bold mb-4">
|
||||
<iconify-icon icon="lucide:hard-drive" height="20"></iconify-icon>
|
||||
<span>Memory</span>
|
||||
</div>
|
||||
|
||||
<%= render "projects/services/resource_constraints/memory_slider", form: form, resource_constraint:, key: :memory_request %>
|
||||
<%= render "projects/services/resource_constraints/memory_slider", form: form, resource_constraint:, key: :memory_limit %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<div data-controller="resource-constraints--slider" data-resource-constraints--slider-type-value="integer" data-resource-constraints--form-target="<%= key %>Field">
|
||||
<% value = resource_constraint.send(key) %>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<%= check_box_tag "enable_#{key}", "1", value.present?,
|
||||
class: "checkbox checkbox-sm",
|
||||
data: {
|
||||
action: "change->resource-constraints--form#toggleField",
|
||||
resource_constraints__form_field_param: key
|
||||
} %>
|
||||
<span class="text-sm"><%= key.to_s.titleize %></span>
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<%= form.text_field(
|
||||
key,
|
||||
type: "number",
|
||||
class: "input input-sm input-bordered font-mono text-sm w-[125px] text-right mr-2 #{'opacity-50 cursor-not-allowed' if value.blank?}",
|
||||
value: value || '',
|
||||
placeholder: "128",
|
||||
required: false,
|
||||
readonly: value.blank?,
|
||||
data: {
|
||||
resource_constraints__slider_target: "numberInput",
|
||||
resource_constraints__form_target: "input",
|
||||
action: "input->resource-constraints--slider#updateSlider"
|
||||
}
|
||||
) %>
|
||||
<span class="text-gray-200">MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
type="range"
|
||||
min="128"
|
||||
max="32768"
|
||||
step="128"
|
||||
value="<%= value || 128 %>"
|
||||
class="w-full h-2 bg-base-300 rounded-lg appearance-none cursor-pointer slider"
|
||||
disabled="<%= value.blank? %>"
|
||||
data-action="input->resource-constraints--slider#updateValue"
|
||||
data-resource-constraints--slider-target="slider"
|
||||
data-resource-constraints--form-target="slider"
|
||||
>
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>128</span>
|
||||
<span>32768</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<%= turbo_frame_tag "resource_constraint_#{service.id}" do %>
|
||||
<% if (service.resource_constraint.present? && service.resource_constraint.persisted?) || (defined?(show_form) && show_form) %>
|
||||
<% resource_constraint = service.resource_constraint || ResourceConstraint.new(service:) %>
|
||||
<%= form_with(
|
||||
model: resource_constraint,
|
||||
url: project_service_resource_constraint_path(service.project, service),
|
||||
method: resource_constraint.persisted? ? :put : :post,
|
||||
data: { turbo_frame: "resource_constraint_form" }) do |form|
|
||||
%>
|
||||
<%= render "projects/services/resource_constraints/form", form: form, resource_constraint: %>
|
||||
<% if resource_constraint.persisted? %>
|
||||
<div class="form-footer mt-6">
|
||||
<%= form.submit "Update Resource Constraints", class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="form-footer mt-6">
|
||||
<%= link_to "Cancel", project_service_resource_constraint_path(service.project, service), class: "btn btn-outline" %>
|
||||
<%= form.submit "Create Resource Constraints", class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="mt-2">
|
||||
<%= button_to "Delete", project_service_resource_constraint_path(service.project, service), method: :delete, class: "btn btn-error btn-outline" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to "Enable Resource Constraints", new_project_service_resource_constraint_path(service.project, service), class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,87 +0,0 @@
|
||||
<div class="space-y-6" data-controller="resource-constraints--form">
|
||||
<div data-resource-constraints--form-target="container">
|
||||
<!-- CPU Configuration -->
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-16 md:gap-32">
|
||||
<div>
|
||||
<div class="font-medium flex items-center gap-2 font-bold mb-4">
|
||||
<iconify-icon icon="lucide:cpu" height="20"></iconify-icon>
|
||||
<span>CPU</span>
|
||||
</div>
|
||||
|
||||
<div data-controller="resource-constraints--slider" data-resource-constraints--slider-type-value="float">
|
||||
<%= form.text_field(
|
||||
:cpu_limit,
|
||||
type: "number",
|
||||
class: "input input-sm input-bordered font-mono text-sm w-[125px] text-right mr-2",
|
||||
value: resource_constraint.cpu_limit.present? ? (resource_constraint.cpu_limit / 1000.0) : nil,
|
||||
step: :any,
|
||||
required: true,
|
||||
placeholder: "0.5",
|
||||
data: {
|
||||
resource_constraints__slider_target: "numberInput",
|
||||
action: "input->resource-constraints--slider#updateSlider"
|
||||
}
|
||||
) %>
|
||||
<span class="text-gray-200">cores</span>
|
||||
<div class="mt-4">
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="16"
|
||||
step="0.1"
|
||||
value="<%= resource_constraint.cpu_limit.present? ? (resource_constraint.cpu_limit / 1000.0) : 0.5 %>"
|
||||
class="w-full h-2 bg-base-300 rounded-lg appearance-none cursor-pointer slider"
|
||||
data-action="input->resource-constraints--slider#updateValue"
|
||||
data-resource-constraints--slider-target="slider"
|
||||
>
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>0.1</span>
|
||||
<span>16.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium flex items-center gap-2 font-bold mb-4">
|
||||
<iconify-icon icon="lucide:hard-drive" height="20"></iconify-icon>
|
||||
<span>Memory</span>
|
||||
</div>
|
||||
|
||||
<div data-controller="resource-constraints--slider" data-resource-constraints--slider-type-value="integer">
|
||||
<%= form.text_field(
|
||||
:memory_limit,
|
||||
type: "number",
|
||||
class: "input input-sm input-bordered font-mono text-sm w-[125px] text-right mr-2",
|
||||
value: resource_constraint.cpu_limit,
|
||||
placeholder: "128",
|
||||
required: true,
|
||||
data: {
|
||||
resource_constraints__slider_target: "numberInput",
|
||||
action: "input->resource-constraints--slider#updateSlider"
|
||||
}
|
||||
) %>
|
||||
<span class="text-gray-200">MB</span>
|
||||
<div class="mt-4">
|
||||
<input
|
||||
type="range"
|
||||
min="128"
|
||||
max="32768"
|
||||
step="128"
|
||||
value="<%= resource_constraint.memory_limit || 128 %>"
|
||||
class="w-full h-2 bg-base-300 rounded-lg appearance-none cursor-pointer slider"
|
||||
data-action="input->resource-constraints--slider#updateValue"
|
||||
data-resource-constraints--slider-target="slider"
|
||||
>
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>128</span>
|
||||
<span>32768</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
<%= turbo_frame_tag "resource_constraint_form" do %>
|
||||
<h3 class="text-lg font-bold mb-4">Edit Resource Constraints</h3>
|
||||
<%
|
||||
url = case @constrainable_type
|
||||
when 'add_on'
|
||||
add_on_resource_constraint_path(@constrainable)
|
||||
when 'project'
|
||||
project_resource_constraint_path(@constrainable)
|
||||
when 'service'
|
||||
project_service_resource_constraint_path(@project, @constrainable)
|
||||
end
|
||||
%>
|
||||
<%= form_with(model: @resource_constraint, url: url, method: :patch, data: { turbo_frame: "resource_constraint_form" }) do |form| %>
|
||||
<%= render "resource_constraints/form", form: form, resource_constraint: @resource_constraint %>
|
||||
<div class="form-footer mt-6 flex gap-2">
|
||||
<%= form.submit "Update Resource Constraints", class: "btn btn-primary", data: { turbo: false } %>
|
||||
<%= button_to "Remove Constraints", url, method: :delete, class: "btn btn-error btn-outline", form: { data: { turbo_confirm: "Are you sure you want to remove resource constraints?" } } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,19 +0,0 @@
|
||||
<%= turbo_frame_tag "resource_constraint_form" do %>
|
||||
<h3 class="text-lg font-bold mb-4">New Resource Constraints</h3>
|
||||
<%
|
||||
url = case @constrainable_type
|
||||
when 'add_on'
|
||||
add_on_resource_constraint_path(@constrainable)
|
||||
when 'project'
|
||||
project_resource_constraint_path(@constrainable)
|
||||
when 'service'
|
||||
project_service_resource_constraint_path(@project, @constrainable)
|
||||
end
|
||||
%>
|
||||
<%= form_with(model: @resource_constraint, url: url, method: :post, data: { turbo_frame: "resource_constraint_form" }) do |form| %>
|
||||
<%= render "resource_constraints/form", form: form, resource_constraint: @resource_constraint %>
|
||||
<div class="form-footer mt-6">
|
||||
<%= form.submit "Create Resource Constraints", class: "btn btn-primary", data: { turbo: false } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,9 +0,0 @@
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
<a href="<%= project_service_path(service.project, service, tab: 'overview') %>" role="tab" class="tab <%= 'tab-active' if tab == 'overview' %>">Overview</a>
|
||||
<% if service.web_service? %>
|
||||
<a href="<%= project_service_path(service.project, service, tab: 'networking') %>" role="tab" class="tab <%= 'tab-active' if tab == 'networking' %>">Networking</a>
|
||||
<% end %>
|
||||
<a href="<%= project_service_path(service.project, service, tab: 'advanced') %>" role="tab" class="tab <%= 'tab-active' if tab == 'advanced' %>">Advanced</a>
|
||||
</div>
|
||||
|
||||
<%= yield %>
|
||||
@@ -65,7 +65,6 @@ Rails.application.routes.draw do
|
||||
resource :metrics, only: [ :show ], module: :add_ons
|
||||
resources :endpoints, only: %i[edit update], module: :add_ons
|
||||
resources :processes, only: %i[index show], module: :add_ons
|
||||
resource :resource_constraint, only: %i[new create edit update destroy]
|
||||
end
|
||||
|
||||
resources :providers, only: %i[index new create destroy]
|
||||
@@ -79,15 +78,14 @@ Rails.application.routes.draw do
|
||||
resources :project_forks, only: %i[index edit create], module: :projects
|
||||
resources :volumes, only: %i[index new create destroy], module: :projects
|
||||
resources :processes, only: %i[index show create destroy], module: :projects
|
||||
resource :resource_constraint, only: %i[new create edit update destroy]
|
||||
resources :services, only: %i[index new create destroy update show], module: :projects do
|
||||
resource :resource_constraint, only: %i[show new create update destroy], module: :services
|
||||
resources :jobs, only: %i[create], module: :services
|
||||
resources :domains, only: %i[create destroy], module: :services do
|
||||
collection do
|
||||
post :check_dns
|
||||
end
|
||||
end
|
||||
resource :resource_constraint, only: %i[new create edit update destroy]
|
||||
end
|
||||
resources :metrics, only: [ :index ], module: :projects
|
||||
resources :project_add_ons, only: %i[create destroy], module: :projects
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class CreateResourceConstraints < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :resource_constraints do |t|
|
||||
t.references :constrainable, polymorphic: true, null: false, index: true
|
||||
t.references :service, null: false, index: true
|
||||
t.bigint :cpu_request
|
||||
t.bigint :cpu_limit
|
||||
t.bigint :memory_request
|
||||
|
||||
13
db/schema.rb
generated
13
db/schema.rb
generated
@@ -426,7 +426,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_14_025053) do
|
||||
t.text "postdestroy_command"
|
||||
t.bigint "project_fork_cluster_id"
|
||||
t.integer "project_fork_status", default: 0
|
||||
t.string "docker_command"
|
||||
t.index ["cluster_id"], name: "index_projects_on_cluster_id"
|
||||
end
|
||||
|
||||
@@ -447,6 +446,18 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_14_025053) do
|
||||
t.index ["user_id"], name: "index_providers_on_user_id"
|
||||
end
|
||||
|
||||
create_table "resource_constraints", force: :cascade do |t|
|
||||
t.bigint "service_id", null: false
|
||||
t.bigint "cpu_request"
|
||||
t.bigint "cpu_limit"
|
||||
t.bigint "memory_request"
|
||||
t.bigint "memory_limit"
|
||||
t.integer "gpu_request"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["service_id"], name: "index_resource_constraints_on_service_id"
|
||||
end
|
||||
|
||||
create_table "services", force: :cascade do |t|
|
||||
t.bigint "project_id", null: false
|
||||
t.integer "service_type", null: false
|
||||
|
||||
@@ -31,34 +31,6 @@ spec:
|
||||
mountPath: <%= volume.mount_path %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% resource_constraint = project.resource_constraint %>
|
||||
<% if resource_constraint.present? %>
|
||||
resources:
|
||||
<% if resource_constraint.cpu_request.present? || resource_constraint.memory_request.present? || resource_constraint.gpu_request.present? %>
|
||||
requests:
|
||||
<% if resource_constraint.cpu_request.present? %>
|
||||
cpu: "<%= resource_constraint.cpu_request_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.memory_request.present? %>
|
||||
memory: "<%= resource_constraint.memory_request_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.gpu_request.present? && resource_constraint.gpu_request > 0 %>
|
||||
nvidia.com/gpu: <%= resource_constraint.gpu_request %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if resource_constraint.cpu_limit.present? || resource_constraint.memory_limit.present? %>
|
||||
limits:
|
||||
<% if resource_constraint.cpu_limit.present? %>
|
||||
cpu: "<%= resource_constraint.cpu_limit_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.memory_limit.present? %>
|
||||
memory: "<%= resource_constraint.memory_limit_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.gpu_request.present? && resource_constraint.gpu_request > 0 %>
|
||||
nvidia.com/gpu: <%= resource_constraint.gpu_request %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
imagePullSecrets:
|
||||
- name: dockerconfigjson-github-com
|
||||
<% if @project.volumes.present? %>
|
||||
|
||||
@@ -21,34 +21,6 @@ spec:
|
||||
mountPath: <%= volume.mount_path %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% resource_constraint = project.resource_constraint %>
|
||||
<% if resource_constraint.present? %>
|
||||
resources:
|
||||
<% if resource_constraint.cpu_request.present? || resource_constraint.memory_request.present? || resource_constraint.gpu_request.present? %>
|
||||
requests:
|
||||
<% if resource_constraint.cpu_request.present? %>
|
||||
cpu: "<%= resource_constraint.cpu_request_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.memory_request.present? %>
|
||||
memory: "<%= resource_constraint.memory_request_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.gpu_request.present? && resource_constraint.gpu_request > 0 %>
|
||||
nvidia.com/gpu: <%= resource_constraint.gpu_request %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if resource_constraint.cpu_limit.present? || resource_constraint.memory_limit.present? %>
|
||||
limits:
|
||||
<% if resource_constraint.cpu_limit.present? %>
|
||||
cpu: "<%= resource_constraint.cpu_limit_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.memory_limit.present? %>
|
||||
memory: "<%= resource_constraint.memory_limit_formatted %>"
|
||||
<% end %>
|
||||
<% if resource_constraint.gpu_request.present? && resource_constraint.gpu_request > 0 %>
|
||||
nvidia.com/gpu: <%= resource_constraint.gpu_request %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: <%= project.name %>
|
||||
|
||||
@@ -39,7 +39,6 @@ RSpec.describe Projects::Update do
|
||||
expect(result.project.branch).to eq('develop')
|
||||
expect(result.project.build_configuration.context_directory).to eq('./app')
|
||||
expect(result.project.repository_url).to eq('updated/repo')
|
||||
expect(result.project.docker_command).to eq('bundle exec rails s')
|
||||
expect(result.project.build_configuration.dockerfile_path).to eq('docker/Dockerfile')
|
||||
end
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ResourceConstraints::Save do
|
||||
let(:project) { create(:project) }
|
||||
let(:resource_constraint) { build(:resource_constraint, :with_project, constrainable: project) }
|
||||
let(:service) { create(:service) }
|
||||
let(:resource_constraint) { build(:resource_constraint, service: service) }
|
||||
|
||||
describe '.call' do
|
||||
context 'with valid CPU core values' do
|
||||
@@ -106,7 +106,7 @@ RSpec.describe ResourceConstraints::Save do
|
||||
end
|
||||
|
||||
context 'with nil CPU values' do
|
||||
let(:resource_constraint) { ResourceConstraint.new(constrainable: project) }
|
||||
let(:resource_constraint) { ResourceConstraint.new(service: service) }
|
||||
let(:params) do
|
||||
{
|
||||
memory_request: '512',
|
||||
@@ -165,5 +165,35 @@ RSpec.describe ResourceConstraints::Save do
|
||||
expect(resource_constraint.gpu_request).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with blank string values' do
|
||||
let(:resource_constraint) do
|
||||
create(:resource_constraint,
|
||||
service: service,
|
||||
cpu_request: 1000,
|
||||
cpu_limit: 2000,
|
||||
memory_request: 512,
|
||||
memory_limit: 1024)
|
||||
end
|
||||
let(:params) do
|
||||
{
|
||||
cpu_request: '',
|
||||
cpu_limit: '',
|
||||
memory_request: '',
|
||||
memory_limit: ''
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.execute(resource_constraint: resource_constraint, params: params) }
|
||||
|
||||
it 'converts blank strings to nil' do
|
||||
result = subject
|
||||
expect(result).to be_success
|
||||
expect(resource_constraint.cpu_request).to be_nil
|
||||
expect(resource_constraint.cpu_limit).to be_nil
|
||||
expect(resource_constraint.memory_request).to be_nil
|
||||
expect(resource_constraint.memory_limit).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,38 +2,29 @@
|
||||
#
|
||||
# Table name: resource_constraints
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# constrainable_type :string not null
|
||||
# cpu_limit :bigint
|
||||
# cpu_request :bigint
|
||||
# gpu_request :integer
|
||||
# memory_limit :bigint
|
||||
# memory_request :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# constrainable_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# cpu_limit :bigint
|
||||
# cpu_request :bigint
|
||||
# gpu_request :integer
|
||||
# memory_limit :bigint
|
||||
# memory_request :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# service_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_resource_constraints_on_constrainable (constrainable_type,constrainable_id)
|
||||
# index_resource_constraints_on_service_id (service_id)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :resource_constraint do
|
||||
association :constrainable, factory: :service
|
||||
service
|
||||
cpu_request { 500 } # 500m
|
||||
cpu_limit { 1000 } # 1 CPU
|
||||
memory_request { 536870912 } # 512Mi
|
||||
memory_limit { 1073741824 } # 1Gi
|
||||
gpu_request { 0 }
|
||||
|
||||
trait :with_project do
|
||||
association :constrainable, factory: :project
|
||||
end
|
||||
|
||||
trait :with_add_on do
|
||||
association :constrainable, factory: :add_on
|
||||
end
|
||||
|
||||
trait :high_resources do
|
||||
cpu_request { 2000 } # 2 CPU
|
||||
cpu_limit { 4000 } # 4 CPU
|
||||
|
||||
@@ -2,20 +2,19 @@
|
||||
#
|
||||
# Table name: resource_constraints
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# constrainable_type :string not null
|
||||
# cpu_limit :bigint
|
||||
# cpu_request :bigint
|
||||
# gpu_request :integer
|
||||
# memory_limit :bigint
|
||||
# memory_request :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# constrainable_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# cpu_limit :bigint
|
||||
# cpu_request :bigint
|
||||
# gpu_request :integer
|
||||
# memory_limit :bigint
|
||||
# memory_request :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# service_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_resource_constraints_on_constrainable (constrainable_type,constrainable_id)
|
||||
# index_resource_constraints_on_service_id (service_id)
|
||||
#
|
||||
require 'rails_helper'
|
||||
|
||||
@@ -23,217 +22,37 @@ RSpec.describe ResourceConstraint, type: :model do
|
||||
let(:resource_constraint) { build(:resource_constraint) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:constrainable) }
|
||||
it { is_expected.to belong_to(:service) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
describe 'cpu_request' do
|
||||
it 'accepts valid integer values' do
|
||||
resource_constraint.cpu_request = 1000
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
it 'validates numericality of resource fields' do
|
||||
resource_constraint.cpu_request = -100
|
||||
resource_constraint.memory_limit = -500
|
||||
resource_constraint.gpu_request = -1
|
||||
|
||||
it 'accepts nil values' do
|
||||
resource_constraint.cpu_request = nil
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects negative values' do
|
||||
resource_constraint.cpu_request = -100
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:cpu_request]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'cpu_limit' do
|
||||
it 'accepts valid integer values' do
|
||||
resource_constraint.cpu_limit = 2000
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'accepts nil values' do
|
||||
resource_constraint.cpu_limit = nil
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects negative values' do
|
||||
resource_constraint.cpu_limit = -100
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:cpu_limit]).to be_present
|
||||
end
|
||||
|
||||
it 'must be greater than or equal to cpu_request' do
|
||||
resource_constraint.cpu_request = 1000
|
||||
resource_constraint.cpu_limit = 500
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:cpu_limit]).to include("must be greater than or equal to CPU request")
|
||||
end
|
||||
|
||||
it 'is valid when cpu_limit equals cpu_request' do
|
||||
resource_constraint.cpu_request = 1000
|
||||
resource_constraint.cpu_limit = 1000
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'is valid when cpu_limit is greater than cpu_request' do
|
||||
resource_constraint.cpu_request = 500
|
||||
resource_constraint.cpu_limit = 1000
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'is valid when cpu_request is nil' do
|
||||
resource_constraint.cpu_request = nil
|
||||
resource_constraint.cpu_limit = 1000
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'is valid when cpu_limit is nil' do
|
||||
resource_constraint.cpu_request = 1000
|
||||
resource_constraint.cpu_limit = nil
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe 'memory_request' do
|
||||
it 'accepts valid integer values' do
|
||||
resource_constraint.memory_request = 1073741824
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'accepts nil values' do
|
||||
resource_constraint.memory_request = nil
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects negative values' do
|
||||
resource_constraint.memory_request = -100
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:memory_request]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'memory_limit' do
|
||||
it 'accepts valid integer values' do
|
||||
resource_constraint.memory_limit = 2147483648
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'accepts nil values' do
|
||||
resource_constraint.memory_limit = nil
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects negative values' do
|
||||
resource_constraint.memory_limit = -100
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:memory_limit]).to be_present
|
||||
end
|
||||
|
||||
it 'must be greater than or equal to memory_request' do
|
||||
resource_constraint.memory_request = 1073741824 # 1Gi
|
||||
resource_constraint.memory_limit = 536870912 # 512Mi
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:memory_limit]).to include("must be greater than or equal to memory request")
|
||||
end
|
||||
|
||||
it 'is valid when memory_limit equals memory_request' do
|
||||
resource_constraint.memory_request = 1073741824
|
||||
resource_constraint.memory_limit = 1073741824
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'is valid when memory_limit is greater than memory_request' do
|
||||
resource_constraint.memory_request = 536870912
|
||||
resource_constraint.memory_limit = 1073741824
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe 'gpu_request' do
|
||||
it 'accepts valid integer values' do
|
||||
resource_constraint.gpu_request = 2
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'accepts nil values' do
|
||||
resource_constraint.gpu_request = nil
|
||||
expect(resource_constraint).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects negative values' do
|
||||
resource_constraint.gpu_request = -1
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:gpu_request]).to be_present
|
||||
end
|
||||
expect(resource_constraint).not_to be_valid
|
||||
expect(resource_constraint.errors[:cpu_request]).to be_present
|
||||
expect(resource_constraint.errors[:memory_limit]).to be_present
|
||||
expect(resource_constraint.errors[:gpu_request]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'string setters' do
|
||||
it 'converts cpu string values to integers' do
|
||||
resource_constraint.cpu_request = "500m"
|
||||
resource_constraint.cpu_limit = "2"
|
||||
expect(resource_constraint.cpu_request).to eq(500)
|
||||
expect(resource_constraint.cpu_limit).to eq(2000)
|
||||
end
|
||||
|
||||
it 'converts memory string values to integers' do
|
||||
resource_constraint.memory_request = "512Mi"
|
||||
resource_constraint.memory_limit = "1Gi"
|
||||
expect(resource_constraint.memory_request).to eq(536870912)
|
||||
expect(resource_constraint.memory_limit).to eq(1073741824)
|
||||
end
|
||||
|
||||
it 'accepts integer values directly' do
|
||||
resource_constraint.cpu_request = 1000
|
||||
resource_constraint.memory_request = 1073741824
|
||||
expect(resource_constraint.cpu_request).to eq(1000)
|
||||
expect(resource_constraint.memory_request).to eq(1073741824)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'formatted getters' do
|
||||
it 'returns formatted cpu values' do
|
||||
describe 'formatted methods' do
|
||||
it 'formats CPU values using millicores' do
|
||||
resource_constraint.cpu_request = 500
|
||||
resource_constraint.cpu_limit = 2000
|
||||
|
||||
expect(resource_constraint.cpu_request_formatted).to eq("500m")
|
||||
expect(resource_constraint.cpu_limit_formatted).to eq("2000m")
|
||||
end
|
||||
|
||||
it 'returns formatted memory values' do
|
||||
resource_constraint.memory_request = 536870912 # 512Mi
|
||||
resource_constraint.memory_limit = 1073741824 # 1Gi
|
||||
expect(resource_constraint.memory_request_formatted).to eq("512.0Mi")
|
||||
expect(resource_constraint.memory_limit_formatted).to eq("1.0Gi")
|
||||
end
|
||||
it 'formats memory values using Kubernetes units' do
|
||||
resource_constraint.memory_request = 536870912
|
||||
resource_constraint.memory_limit = 1073741824
|
||||
|
||||
it 'returns nil for nil values' do
|
||||
resource_constraint.cpu_request = nil
|
||||
resource_constraint.memory_limit = nil
|
||||
expect(resource_constraint.cpu_request_formatted).to be_nil
|
||||
expect(resource_constraint.memory_limit_formatted).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'polymorphic associations' do
|
||||
it 'can be associated with a Service' do
|
||||
service = create(:service)
|
||||
resource_constraint = create(:resource_constraint, constrainable: service)
|
||||
expect(resource_constraint.constrainable).to eq(service)
|
||||
expect(service.resource_constraint).to eq(resource_constraint)
|
||||
end
|
||||
|
||||
it 'can be associated with a Project' do
|
||||
project = create(:project)
|
||||
resource_constraint = create(:resource_constraint, constrainable: project)
|
||||
expect(resource_constraint.constrainable).to eq(project)
|
||||
expect(project.resource_constraint).to eq(resource_constraint)
|
||||
end
|
||||
|
||||
it 'can be associated with an AddOn' do
|
||||
add_on = create(:add_on)
|
||||
resource_constraint = create(:resource_constraint, constrainable: add_on)
|
||||
expect(resource_constraint.constrainable).to eq(add_on)
|
||||
expect(add_on.resource_constraint).to eq(resource_constraint)
|
||||
expect(resource_constraint.memory_request_formatted).to eq("512Mi")
|
||||
expect(resource_constraint.memory_limit_formatted).to eq("1Gi")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user