resource constraints and services refactor

This commit is contained in:
Chris
2025-11-15 19:30:46 -08:00
parent b74af7fe67
commit 748ae78dc9
39 changed files with 530 additions and 675 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = ''
}
}
}
}

View 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
})
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -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" />

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %>

View 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>

View File

@@ -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 %>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %>

View File

@@ -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>

View File

@@ -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 %>

View File

@@ -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 %>

View File

@@ -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 %>

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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? %>

View File

@@ -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 %>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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