class ClustersController < ApplicationController before_action :set_cluster, only: [ :show, :edit, :update, :destroy, :test_connection, :download_kubeconfig, :logs, :download_yaml, :retry_install, :transfer_ownership ] # GET /clusters def index sortable_column = params[:sort] || "created_at" clusters = Clusters::List.call(account_user: current_account_user, params: params).clusters @pagy, @clusters = pagy(clusters.order(sortable_column => "asc")) respond_to do |format| format.html format.json { render json: @clusters.map { |c| { id: c.id, name: c.name } } } end # Uncomment to authorize with Pundit # authorize @clusters end # GET /clusters/1 or /clusters/1.json def show end # GET /clusters/new def new @cluster = Cluster.new # Uncomment to authorize with Pundit # authorize @cluster end # GET /clusters/1/edit def edit end def logs end def check_k3s_ip_address # Check if the IP address is reachable at port 6443 ip_address = params[:ip_address] port = 6443 timeout = 5 # seconds begin Timeout.timeout(timeout) do TCPSocket.new(ip_address, port).close end render json: { success: true } rescue Errno::ECONNREFUSED render json: { success: false, error: "Connection refused" }, status: :unprocessable_entity rescue Errno::EHOSTUNREACH render json: { success: false, error: "Host unreachable" }, status: :unprocessable_entity rescue Timeout::Error render json: { success: false, error: "Connection timed out" }, status: :unprocessable_entity rescue StandardError => e render json: { success: false, error: e.message }, status: :unprocessable_entity end end def retry_install Clusters::InstallJob.perform_later(@cluster, current_user) redirect_to @cluster, notice: "Retrying installation for cluster..." end def test_connection client = K8::Client.new(K8::Connection.new(@cluster, current_user)) if client.can_connect? render turbo_stream: turbo_stream.replace("test_connection_frame", partial: "clusters/connection_success") else render turbo_stream: turbo_stream.replace("test_connection_frame", partial: "clusters/connection_failed") end end def export(cluster, namespace, yaml_content, zip) parsed = YAML.safe_load(yaml_content) parsed['items'].each do |item| name = item['metadata']['name'] zip.put_next_entry("#{cluster}/#{namespace}/#{name}.yaml") zip.write(item.to_yaml) end end def download_yaml require 'zip' stringio = Zip::OutputStream.write_buffer do |zio| @cluster.projects.each do |project| # Create a directory for each project # Export services, deployments, ingress and cron jobs from a kubernetes namespace %w[services deployments ingress cronjobs].each do |resource| yaml_content = K8::Kubectl.new( K8::Connection.new(@cluster, current_user) ).call("get #{resource} -n #{project.namespace} -o yaml") export(@cluster.name, project.namespace, yaml_content, zio) end end end stringio.rewind # Send the zip file to the user send_data(stringio.read, filename: "#{@cluster.name}.zip", type: "application/zip" ) end def download_kubeconfig connection = K8::Connection.new(@cluster, current_user) send_data connection.kubeconfig.to_yaml, filename: "#{@cluster.name}-kubeconfig.yml", type: "application/yaml" end # POST /clusters or /clusters.json def create @cluster = current_account.clusters.new(cluster_params) result = Clusters::Create.call(@cluster, current_user) # Uncomment to authorize with Pundit # authorize @cluster respond_to do |format| if result.success? # Kick off cluster job Clusters::InstallJob.perform_later(@cluster, current_user) format.html { redirect_to @cluster, notice: "Cluster was successfully created." } format.json { render :show, status: :created, location: @cluster } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @cluster.errors, status: :unprocessable_entity } end end end # PATCH/PUT /clusters/1 or /clusters/1.json def update respond_to do |format| if @cluster.update(cluster_params) format.html { redirect_to @cluster, notice: "Cluster was successfully updated." } format.json { render :show, status: :ok, location: @cluster } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @cluster.errors, status: :unprocessable_entity } end end end # DELETE /clusters/1 or /clusters/1.json def destroy @cluster.destroy! respond_to do |format| format.html { redirect_to clusters_url, status: :see_other, notice: "Cluster was successfully destroyed." } format.json { head :no_content } end end def edit end def destroy Clusters::DestroyJob.perform_later(@cluster, current_user) respond_to do |format| format.html { redirect_to clusters_url, status: :see_other, notice: "Cluster is being deleted... It may take a few minutes to complete." } format.json { head :no_content } end end def transfer_ownership @cluster.update(account_id: params[:cluster][:account_id]) redirect_to cluster_url(@cluster), notice: "Cluster ownership transferred successfully" end private # Use callbacks to share common setup or constraints between actions. def set_cluster clusters = Clusters::VisibleToUser.execute(account_user: current_account_user).clusters @cluster = clusters.find(params[:id]) # Uncomment to authorize with Pundit # authorize @cluster rescue ActiveRecord::RecordNotFound redirect_to clusters_path end # Only allow a list of trusted parameters through. def cluster_params # Handle kubeconfig from YAML editor if params[:cluster][:kubeconfig_yaml_format] == "true" && params[:cluster][:kubeconfig].present? params[:cluster][:kubeconfig] = YAML.safe_load(params[:cluster][:kubeconfig]) elsif 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, :cluster_type, kubeconfig: {}) end end