diff --git a/app/controllers/clusters_controller.rb b/app/controllers/clusters_controller.rb index bce59795..10a13b80 100644 --- a/app/controllers/clusters_controller.rb +++ b/app/controllers/clusters_controller.rb @@ -34,7 +34,7 @@ class ClustersController < ApplicationController end def retry_install - InstallClusterJob.perform_later(@cluster) + Clusters::InstallJob.perform_later(@cluster) redirect_to @cluster, notice: "Retrying installation for cluster..." end @@ -93,7 +93,7 @@ class ClustersController < ApplicationController respond_to do |format| if @cluster.save # Kick off cluster job - InstallClusterJob.perform_later(@cluster) + Clusters::InstallJob.perform_later(@cluster) format.html { redirect_to @cluster, notice: "Cluster was successfully created." } format.json { render :show, status: :created, location: @cluster } else @@ -139,13 +139,30 @@ class ClustersController < ApplicationController # Only allow a list of trusted parameters through. def cluster_params - if params[:cluster][:kubeconfig].present? - kubeconfig_file = params[:cluster][:kubeconfig] + if params[:cluster][:cluster_type] == "k3s" + ip_address = params[:cluster][:ip_address] + kubeconfig_output = params[:cluster][:kubeconfig_output] + if ip_address.blank? || kubeconfig_output.blank? + message = "IP address and kubeconfig output are required for K3s clusters" + flash[:error] = message + raise message + end + + begin + data = YAML.safe_load(kubeconfig_output) + data["clusters"][0]["cluster"]["server"] = "https://#{ip_address}:6443" + rescue StandardError => e + message = "Invalid kubeconfig output" + flash[:error] = message + raise message + end + params[:cluster][:kubeconfig] = data + elsif (kubeconfig_file = params[:cluster][:kubeconfig_file]).present? yaml_content = kubeconfig_file.read params[:cluster][:kubeconfig] = YAML.safe_load(yaml_content) end - params.require(:cluster).permit(:name, kubeconfig: {}) + params.require(:cluster).permit(:name, :cluster_type, kubeconfig: {}) end end diff --git a/app/javascript/controllers/k3s_instructions_controller.js b/app/javascript/controllers/k3s_instructions_controller.js new file mode 100644 index 00000000..710a58e7 --- /dev/null +++ b/app/javascript/controllers/k3s_instructions_controller.js @@ -0,0 +1,14 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["instructions"] + + update(event) { + // Validate it is a valid IP address + if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(event.target.value)) { + return + } + this.instructionsTarget.classList.remove("hidden") + } +} + diff --git a/app/jobs/install_cluster_job.rb b/app/jobs/clusters/install_job.rb similarity index 67% rename from app/jobs/install_cluster_job.rb rename to app/jobs/clusters/install_job.rb index 85f893a9..befd6b1f 100644 --- a/app/jobs/install_cluster_job.rb +++ b/app/jobs/clusters/install_job.rb @@ -1,4 +1,4 @@ -class InstallClusterJob < ApplicationJob +class Clusters::InstallJob < ApplicationJob queue_as :default def perform(cluster) diff --git a/app/models/cluster.rb b/app/models/cluster.rb index 5e9542f4..16e61002 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -2,13 +2,14 @@ # # Table name: clusters # -# id :bigint not null, primary key -# kubeconfig :jsonb not null -# name :string not null -# status :integer default("initializing"), not null -# created_at :datetime not null -# updated_at :datetime not null -# account_id :bigint not null +# id :bigint not null, primary key +# cluster_type :integer default("k8s") +# kubeconfig :jsonb not null +# name :string not null +# status :integer default("initializing"), not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null # # Indexes # @@ -37,4 +38,8 @@ class Cluster < ApplicationRecord running: 2, failed: 3 } + enum :cluster_type, { + k8s: 0, + k3s: 1, + } end diff --git a/app/models/service.rb b/app/models/service.rb index 56512978..583962c0 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -6,6 +6,7 @@ # allow_public_networking :boolean default(FALSE) # command :string # container_port :integer default(3000) +# description :text # healthcheck_url :string # last_health_checked_at :datetime # name :string not null diff --git a/app/services/k8/client.rb b/app/services/k8/client.rb index efe39880..ec111e0d 100644 --- a/app/services/k8/client.rb +++ b/app/services/k8/client.rb @@ -64,25 +64,33 @@ module K8 Kubeclient::Client.new( cluster_info["server"], "v1", - ssl_options: ssl_options(cluster_info), + ssl_options: ssl_options(user_info, cluster_info), auth_options: auth_options(user_info) ) end - def ssl_options(_cluster_info) - { verify_ssl: OpenSSL::SSL::VERIFY_NONE } + def ssl_options(user_info, cluster_info) + _ssl_options = {} + if user_info["client-certificate-data"] && user_info["client-key-data"] + + begin + _ssl_options[:client_key] = OpenSSL::PKey::RSA.new(Base64.decode64(user_info["client-key-data"])) + rescue OpenSSL::PKey::RSAError + _ssl_options[:client_key] = OpenSSL::PKey::EC.new(Base64.decode64(user_info["client-key-data"])) + end + _ssl_options[:client_cert] = + OpenSSL::X509::Certificate.new(Base64.decode64(user_info["client-certificate-data"])) + _ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE + end # if cluster_info['certificate-authority-data'] # ssl_options[:ca_file] = write_temp_file(Base64.decode64(cluster_info['certificate-authority-data'])) # end + _ssl_options end def auth_options(user_info) auth_options = {} - if user_info["client-certificate-data"] && user_info["client-key-data"] - auth_options[:client_cert] = - OpenSSL::X509::Certificate.new(Base64.decode64(user_info["client-certificate-data"])) - auth_options[:client_key] = OpenSSL::PKey::RSA.new(Base64.decode64(user_info["client-key-data"])) - elsif user_info["token"] + if user_info["token"] auth_options[:bearer_token] = user_info["token"] end auth_options diff --git a/app/views/clusters/_form.html.erb b/app/views/clusters/_form.html.erb index 36343a1f..1527d3ec 100644 --- a/app/views/clusters/_form.html.erb +++ b/app/views/clusters/_form.html.erb @@ -5,34 +5,27 @@ <%= form.text_field :name, class: "input input-bordered", value: RandomNameGenerator.generate_name %> - <% if defined?(show_instructions) %>
- - <%= render "clusters/instructions/cards/digitalocean" %> - <%= render "clusters/instructions/cards/linode" %> - <%= render "clusters/instructions/cards/other" %> + <%= form.collection_select :cluster_type, + Cluster.cluster_types.keys, + :to_s, + :titleize, + { }, + { class: "select select-bordered hidden", data: { new_add_ons_target: "input" } } + %> + <%= render "clusters/cluster_types/cards/k8s" %> + <%= render "clusters/cluster_types/cards/k3s" %>
- <% end %> - -
- <%= form.label :kubeconfig %> - <%= form.file_field :kubeconfig, class: "file-input w-full" %> -
diff --git a/db/migrate/20241215025029_add_cluster_type_to_clusters.rb b/db/migrate/20241215025029_add_cluster_type_to_clusters.rb new file mode 100644 index 00000000..fa794a9a --- /dev/null +++ b/db/migrate/20241215025029_add_cluster_type_to_clusters.rb @@ -0,0 +1,6 @@ +class AddClusterTypeToClusters < ActiveRecord::Migration[7.2] + def change + add_column :clusters, :cluster_type, :integer, default: 0 + Cluster.update_all(cluster_type: :k8s) + end +end diff --git a/db/schema.rb b/db/schema.rb index 72ad16b9..67fe67d6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_12_07_234211) do +ActiveRecord::Schema[7.2].define(version: 2024_12_15_025029) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -113,6 +113,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_07_234211) do t.integer "status", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "cluster_type", default: 0 t.index ["account_id"], name: "index_clusters_on_account_id" end