#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2026, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION = ENV['ONE_LOCATION']

if !ONE_LOCATION
    LIB_LOCATION      = '/usr/lib/one'
    RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
    GEMS_LOCATION     = '/usr/share/one/gems'
else
    LIB_LOCATION      = ONE_LOCATION + '/lib'
    RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
    GEMS_LOCATION     = ONE_LOCATION + '/share/gems'
end

require 'load_opennebula_paths'

$LOAD_PATH << RUBY_LIB_LOCATION
$LOAD_PATH << RUBY_LIB_LOCATION + '/cli'
$LOAD_PATH << LIB_LOCATION      + '/oneks/lib'

USER_AGENT = 'CLI'

require 'json'
require 'tempfile'
require 'command_parser'
require 'opennebula/ks/client'
require 'one_helper'
require 'one_helper/oneks_cluster_helper'
require 'one_helper/oneks_group_helper'
require 'one_helper/oneks_application_helper'

CLUSTER_ID = {
    :name        => 'cluster_id',
    :short       => '-c id',
    :large       => '--cluster-id id',
    :format      => Integer,
    :description => 'OneKS Cluster ID'
}

WAIT = {
    :name        => 'wait',
    :large       => '--wait',
    :description => 'Follow cluster logs after a successful cluster creation',
    :default     => false
}

ALL_LOGS = {
    :name        => 'all',
    :short       => '-a',
    :large       => '--all',
    :description => 'Show the full cluster log history',
    :default     => false
}

FOLLOW_LOGS = {
    :name        => 'follow',
    :short       => '-f',
    :large       => '--follow',
    :description => 'Poll cluster logs continuously',
    :default     => false
}

KUBERNETES_VERSION = {
    :name        => 'kubernetes_version',
    :large       => '--k8s-version version',
    :format      => String,
    :description => 'Kubernetes version to upgrade the cluster to'
}

KUBECONFIG = {
    :name        => 'kubeconfig',
    :short       => '-k',
    :large       => '--kubeconfig',
    :description => 'Show the Cluster kubeconfig',
    :default     => false
}

OBSERVATIONS = {
    :name        => 'observations',
    :short       => '-o',
    :large       => '--observations',
    :description => 'Show the latest Kubernetes observations for a Cluster',
    :default     => false
}

APPLICATION = {
    :name        => 'application',
    :large       => '--app release_name',
    :format      => String,
    :description => '# Show details of an Application installed in a cluster'
}

ALL_APPLICATIONS = {
    :name        => 'all',
    :short       => '-a',
    :large       => '--all',
    :description => 'Include all Application entries, including components or dependencies',
    :default     => false
}

PODS = {
    :name        => 'pods',
    :large       => '--pods',
    :description => 'Show the latest Kubernetes pods for a Cluster or Group',
    :default     => false
}

HISTORIC = {
    :name        => 'historic',
    :short       => '-H',
    :large       => '--historic',
    :description => 'Show the aggregate history for a Cluster or Group',
    :default     => false
}

OPENNEBULA_CLUSTER = {
    :name        => 'opennebula_cluster',
    :large       => '--opennebula-cluster id',
    :format      => Integer,
    :description => 'OpenNebula cluster ID used by OneKS deployment placement'
}

PUBLIC_NETWORK = {
    :name        => 'public_network',
    :large       => '--public-network id',
    :format      => Integer,
    :description => 'Public network ID used by the OneKS readiness check service'
}

PRIVATE_NETWORK = {
    :name        => 'private_network',
    :large       => '--private-network id',
    :format      => Integer,
    :description => 'Private network ID used by the OneKS readiness check service'
}

TARGET = {
    :name        => 'target',
    :large       => '--target count',
    :format      => Integer,
    :description => 'Target number of nodes for group scaling'
}

RELEASE_NAME = {
    :name        => 'release_name',
    :large       => '--release-name name',
    :format      => String,
    :description => 'Helm release name for the Application'
}

TARGET_NAMESPACE = {
    :name        => 'target_namespace',
    :large       => '--target-namespace namespace',
    :format      => String,
    :description => 'Kubernetes namespace where the Application is installed'
}

CREATE_NAMESPACE = {
    :name        => 'create_namespace',
    :large       => '--create-namespace',
    :description => 'Create the target namespace during installation',
    :default     => false
}

RECOVER_RETRY = {
    :name        => 'retry',
    :large       => '--retry',
    :description => 'Retry the failed lifecycle step (default)',
    :default     => false
}

RECOVER_SUCCESS = {
    :name        => 'success',
    :large       => '--success',
    :description => 'Resolve the current lifecycle step as successful',
    :default     => false
}

RECOVER_FAILURE = {
    :name        => 'failure',
    :large       => '--failure',
    :description => 'Resolve the current lifecycle step as failed',
    :default     => false
}

RECOVER_DELETE_DB = {
    :name        => 'delete_db',
    :large       => '--delete-db',
    :description => 'Delete only the Cluster and Group database documents',
    :default     => false
}

CommandParser::CmdParser.new(ARGV) do
    FORMAT = [OpenNebulaHelper::JSON, OpenNebulaHelper::YAML]

    usage <<~USAGE
        OneKS command-line interface for managing Kubernetes clusters in OpenNebula.

        Usage:
            oneks <command> <resource> [<args>] [<options>]

        Run 'oneks help' to list available commands.
        Run 'oneks <command> --help' for detailed command usage.
    USAGE

    version OpenNebulaHelper::ONE_VERSION

    set :option, OneKS::Client::DEFAULT_OPTIONS
    set :option, CommandParser::VERSION
    set :option, CommandParser::HELP

    kscluster = OneKSClusterHelper.new
    ksgroup   = OneKSGroupHelper.new
    ksapp     = OneKSApplicationHelper.new

    supported_resources = ['cluster', 'group'].join(', ')
    all_resources_supported = ['cluster', 'group', 'app'].join(', ')

    ############################################################################
    # Formatters for arguments
    ############################################################################
    set :format, :groupid, OpenNebulaHelper.rname_to_id_desc('GROUP') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'GROUP')
    end

    set :format, :userid, OpenNebulaHelper.rname_to_id_desc('USER') do |arg|
        OpenNebulaHelper.rname_to_id(arg, 'USER')
    end

    set :format, :resource, 'OneKS resource type: cluster(s), group(s), app(s)' do |arg|
        resource = arg.to_s.downcase

        case resource
        when 'cluster', 'clusters'
            [0, 'cluster']
        when 'group', 'groups'
            [0, 'group']
        when 'app', 'apps'
            [0, 'app']
        else
            [-1, "Invalid resource type '#{arg}'. Use 'cluster', 'group', or 'app'."]
        end
    end

    set :format, :resource_id, 'OneKS resource ID' do |arg|
        [0, arg]
    end

    set :format, :clusterid_list, OneKS::Client.list_to_id_desc('OneKS Clusters') do |arg|
        OneKS::Client.list_to_id(arg, :clusters)
    end

    ########################################################################
    # Commands for interacting with OneKS clusters
    ########################################################################

    list_desc = <<-EOT.unindent
        List OneKS resources of the specified resource.
        Supported resources: #{all_resources_supported}.

        Examples:
            # List all clusters
            oneks list clusters

            # List all nodegroups
            oneks list groups

            # List public Applications
            oneks list apps

    EOT

    command :list,
            list_desc,
            :resource,
            :options => FORMAT + CLIHelper::OPTIONS + [
                OpenNebulaHelper::DESCRIBE, ALL_APPLICATIONS
            ] do
        client   = OneKSHelper.client(options)
        resource = args[0]

        case resource
        when 'cluster'
            rc = kscluster.list(client, options)
        when 'group'
            rc = ksgroup.list(client, options)
        when 'app'
            rc = ksapp.list(client, options)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        rc
    rescue Interrupt
        puts
        exit(1)
    end

    top_desc = <<-EOT.unindent
        List the available OneKS resources continuously.
        Supported resources: #{supported_resources}.

        Examples:
            # List all clusters
            oneks top clusters

            # List all nodegroups
            oneks top groups

    EOT

    command :top,
            top_desc,
            :resource,
            :options => FORMAT + [CLIHelper::DELAY] do
        client   = OneKSHelper.client(options)
        resource = args[0]

        Signal.trap('INT') { exit(1) }

        case resource
        when 'cluster'
            rc = kscluster.top(client, options)
        when 'group'
            rc = ksgroup.top(client, options)
        when 'app'
            STDERR.puts('Top is only supported for clusters and groups')
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        rc
    rescue Interrupt
        puts
        exit(1)
    end

    show_desc = <<-EOT.unindent
        Show detailed information about a specific OneKS resource.
        Supported resources: cluster, group, app.

        Examples:
            # Show cluster details
            oneks show cluster <cluster_id>

            # Show the latest cluster observations
            oneks show cluster <cluster_id> --observations

            # # Show details of an Application installed in a cluster
            oneks show cluster <cluster_id> --app <release_name>

            # Show nodegroup details
            oneks show group <group_id>

            # Show a complete Application definition
            oneks show app <application_id>

            # Show aggregate cluster history
            oneks show cluster <cluster_id> --historic

            # Show the aggregate history of the Group's Cluster
            oneks show group <group_id> -H

            # Show the latest pods stored for a nodegroup
            oneks show group <group_id> --pods

            # Show all stored Cluster pods
            oneks show cluster <cluster_id> --pods

    EOT

    command :show,
            show_desc,
            :resource,
            :resource_id,
            :options => FORMAT + [
                KUBECONFIG, OBSERVATIONS, APPLICATION, PODS, HISTORIC
            ] do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]

        if resource != 'app' && !resource_id.to_s.match?(/^\d+$/)
            STDERR.puts "Argument '#{resource_id}' is not a valid ID"
            exit(-1)
        end
        resource_id = resource_id.to_i unless resource == 'app'
        views = [:kubeconfig, :observations, :application, :pods, :historic].select do |view|
            options[view]
        end

        if views.size > 1
            STDERR.puts("Only one show view can be selected: #{views.join(', ')}")
            exit(-1)
        end

        case resource
        when 'cluster'
            rc =
                if options[:kubeconfig]
                    kscluster.kubeconfig(client, resource_id, options)
                elsif options[:observations]
                    kscluster.observations(client, resource_id, options)
                elsif options[:application]
                    kscluster.application(client, resource_id, options[:application], options)
                elsif options[:pods]
                    kscluster.pods(client, resource_id, options)
                elsif options[:historic]
                    kscluster.historic(client, resource_id, options)
                else
                    kscluster.show(client, resource_id, options)
                end
        when 'group'
            if options[:kubeconfig] || options[:observations] || options[:application]
                view = [:kubeconfig, :observations, :application].find do |item|
                    options[item]
                end
                STDERR.puts(
                    "#{view} view is not supported for #{resource}. " \
                    'Only clusters expose it.'
                )
                exit(-1)
            end

            rc =
                if options[:pods]
                    ksgroup.pods(client, resource_id, options)
                elsif options[:historic]
                    ksgroup.historic(client, resource_id, options)
                else
                    ksgroup.show(client, resource_id, options)
                end
        when 'app'
            if views.any?
                STDERR.puts('Cluster and Group show views are not supported for Applications')
                exit(-1)
            end

            rc = ksapp.show(client, resource_id, options)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        rc
    rescue Interrupt
        puts
        exit(1)
    end

    install_desc = <<-EOT.unindent
        Install a catalogue Application in a OneKS Cluster.

        Examples:
            # Install interactively by canonical Application ID
            oneks install app <application_id> --cluster-id <cluster_id>

            # Install non-interactively from a JSON request fragment
            oneks install app <application_id> --cluster-id <cluster_id> --file install.json
    EOT

    command :install,
            install_desc,
            :resource,
            :resource_id,
            :options => [
                CLUSTER_ID, OpenNebulaHelper::FILE, RELEASE_NAME,
                TARGET_NAMESPACE, CREATE_NAMESPACE
            ] do
        resource    = args[0]
        application = args[1]
        cluster_id  = options[:cluster_id]

        unless resource == 'app'
            STDERR.puts('Install is only supported for applications')
            exit(-1)
        end

        if cluster_id.nil?
            STDERR.puts 'A cluster ID is required for Application installation'
            STDERR.puts 'Usage: oneks install app <application> --cluster-id <cluster_id>'
            exit(-1)
        end

        body   = ODSHelper.read_json_input(options[:file])
        client = OneKSHelper.client(options)
        rc     = ksapp.install(client, application, cluster_id, body, options)

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        rc
    rescue Interrupt
        puts
        exit(1)
    end

    create_desc = <<-EOT.unindent
        Create a specific OneKS resource.
        Supported resources: #{supported_resources}.

        Examples:
            # Start creation in interactive mode
            oneks create cluster
            oneks create group --cluster-id <cluster_id>

            # Create and wait for completion (only supported for cluster)
            oneks create cluster --wait

            # Create from a JSON file (non-interactive)
            oneks create cluster --file spec.json
            oneks create group --file spec.json

            # Cluster specs use deployment.cluster.id and deployment.networks.*.id

    EOT

    command :create,
            create_desc,
            :resource,
            :options => [OpenNebulaHelper::FILE, CLUSTER_ID, WAIT] do
        client     = OneKSHelper.client(options)
        resource   = args[0]
        file       = options[:file]
        cluster_id = options[:cluster_id]
        body       = ODSHelper.read_json_input(file)

        case resource
        when 'cluster'
            rc = kscluster.create(client, body, options)
            return rc if kscluster.is_error?(rc)
        when 'group'
            if cluster_id.nil?
                STDERR.puts 'A cluster ID is required for group creation'
                STDERR.puts 'Usage: oneks create group --cluster-id <cluster_id>'
                exit(-1)
            end

            if options[:wait]
                STDERR.puts '--wait is only supported for cluster creation'
                exit(-1)
            end

            rc = ksgroup.create(client, cluster_id, body)
            return rc if ksgroup.is_error?(rc)
        when 'app'
            STDERR.puts('Create is only supported for clusters and groups')
            exit(-1)
        end

        0
    rescue Interrupt
        puts
        exit(1)
    end

    update_desc = <<-EOT.unindent
        Update a specific OneKS resource by ID using the provided data.
        Supported resources: #{supported_resources}.
        Supported attrs: name, description.

        If no input file is provided, the CLI opens the current resource
        in your default editor to apply changes interactively.

        Examples:
            # Update a cluster using the editor
            oneks update cluster <cluster_id>

            # Update a cluster from a JSON file
            oneks update cluster <cluster_id> --file data.json
    EOT

    command :update,
            update_desc,
            :resource,
            :resource_id,
            :options => [OpenNebulaHelper::FILE] do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]
        file        = options[:file]
        body        = ODSHelper.read_json_input(file)

        case resource
        when 'cluster'
            body ||= OneKSClusterHelper.update_from_editor(client, resource_id)
            return body if kscluster.is_error?(body)

            rc = kscluster.update(client, resource_id, body)
            return rc if kscluster.is_error?(rc)
        when 'group'
            body ||= OneKSGroupHelper.update_from_editor(client, resource_id)
            return body if ksgroup.is_error?(body)

            rc = ksgroup.update(client, resource_id, body)
            return rc if ksgroup.is_error?(rc)
        when 'app'
            STDERR.puts('Update is only supported for clusters and groups')
            exit(-1)
        end

        0
    rescue Interrupt
        puts
        exit(1)
    end

    rename_desc = <<-EOT.unindent
        Rename a specific OneKS resource.

        Examples:
            # Rename a cluster
            oneks rename cluster <cluster_id> --name <new_name>

            # Rename a nodegroup
            oneks rename group <group_id> --name <new_name>
    EOT

    command :rename, rename_desc, :resource, :resource_id, :name do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]
        name        = args[2]

        case resource
        when 'cluster'
            rc = kscluster.rename(client, resource_id, name)
        when 'group'
            rc = ksgroup.rename(client, resource_id, name)
        when 'app'
            STDERR.puts('Rename is only supported for clusters and groups')
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end

    chgrp_desc = <<-EOT.unindent
        Change the group of a OneKS cluster.
        Supported resources: cluster.

        Examples:
            # Change the group of a cluster
            oneks chgrp cluster <cluster_id> <group_id>
    EOT

    command :chgrp, chgrp_desc, :resource, :resource_id, :groupid do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]
        group_id    = args[2].to_i

        case resource
        when 'cluster'
            rc = kscluster.chgrp(client, resource_id, group_id)
        when 'group', 'app'
            STDERR.puts(
                "chgrp operation is not supported for #{resource}. " \
                'Only clusters support ownership and permission changes.'
            )
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end

    chown_desc = <<-EOT.unindent
        Change the owner of a OneKS cluster.
        Supported resources: cluster.

        Examples:
            # Change the owner of a cluster
            oneks chown cluster <cluster_id> <user_id>

            # Change the owner and group of a cluster
            oneks chown cluster <cluster_id> <user_id> <group_id>
    EOT

    command :chown, chown_desc, :resource, :resource_id, :userid, [:groupid, nil] do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]
        owner_id    = args[2].to_i
        group_id    = args[3] ? args[3].to_i : nil

        case resource
        when 'cluster'
            rc = kscluster.chown(client, resource_id, owner_id, group_id)
        when 'group', 'app'
            STDERR.puts(
                "chown operation is not supported for #{resource}. " \
                'Only clusters support ownership and permission changes.'
            )
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end

    chmod_desc = <<-EOT.unindent
        Change the permissions of a OneKS cluster.
        Supported resources: cluster.

        Examples:
            # Change cluster permissions
            oneks chmod cluster <cluster_id> 640
    EOT

    command :chmod, chmod_desc, :resource, :resource_id, :octet do
        if !/\A\d+\z/.match(args[2])
            STDERR.puts "Invalid '#{args[2]}' octed permissions"
            exit(-1)
        end

        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]
        octet       = OpenNebulaHelper.to_octet(args[2])

        case resource
        when 'cluster'
            rc = kscluster.chmod(client, resource_id, octet)
        when 'group', 'app'
            STDERR.puts(
                "chmod operation is not supported for #{resource}. " \
                'Only clusters support ownership and permission changes.'
            )
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end

    recover_desc = <<-EOT.unindent
        Recover or manually resolve a Cluster or NodeGroup lifecycle action.
        With no option, the failed action is retried. --delete-db is an emergency
        Cluster operation and does not remove external infrastructure.

        Examples:
            # Retry the failed action (default)
            oneks recover cluster <cluster_id>

            # Resolve the current step explicitly
            oneks recover cluster <cluster_id> --success
            oneks recover cluster <cluster_id> --failure

            # Remove only its persisted OneKS documents
            oneks recover cluster <cluster_id> --delete-db

            # Resolve one active or failed NodeGroup operation
            oneks recover group <group_id> --success
            oneks recover group <group_id> --failure
    EOT

    command :recover,
            recover_desc,
            :resource,
            :resource_id,
            :options => [
                RECOVER_RETRY, RECOVER_SUCCESS, RECOVER_FAILURE, RECOVER_DELETE_DB
            ] do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]

        actions = {
            :retry     => options[:retry],
            :success   => options[:success],
            :failure   => options[:failure],
            :delete_db => options[:delete_db]
        }.select {|_action, selected| selected }.keys

        if actions.size > 1
            STDERR.puts 'Use only one recover action option'
            exit(-1)
        end

        action = actions.first || :retry

        if resource == 'group' && action == :delete_db
            STDERR.puts '--delete-db is only supported for cluster recovery'
            exit(-1)
        end

        rc =
            case resource
            when 'cluster'
                kscluster.recover(client, [resource_id], action)
            when 'group'
                ksgroup.recover(client, resource_id, action)
            when 'app'
                STDERR.puts('Recover is only supported for clusters and groups')
                exit(-1)
            end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end

    delete_desc = <<-EOT.unindent
        Delete a specific OneKS resource.
        Supported resources: #{all_resources_supported}.

        Examples:
            # Delete a cluster
            oneks delete cluster <cluster_id>

            # Delete a nodegroup
            oneks delete group <group_id>

            # Uninstall an Application by release name
            oneks delete app <release_name> --cluster-id <cluster_id>
    EOT

    check_desc = <<-EOT.unindent
        Run the OneKS readiness check service to validate public and private
        network configurations before provisioning the Kubernetes cluster.
        Networks can be provided directly or resolved from an existing cluster.

        Examples:
            # Run the check interactively
            oneks check

            # Check readiness with explicit deployment placement
            oneks check --opennebula-cluster <one_cluster_id> \\
                --public-network <network_id> --private-network <network_id>

            # Check readiness using the configuration from an existing OneKS cluster
            oneks check cluster <cluster_id>
    EOT

    command :check,
            check_desc,
            [:resource, nil],
            [:resource_id, nil],
            :options => [OPENNEBULA_CLUSTER, PUBLIC_NETWORK, PRIVATE_NETWORK] do
        deployment_id   = options[:opennebula_cluster]
        public_network  = options[:public_network]
        private_network = options[:private_network]
        resource        = args[0]
        resource_id     = args[1]

        body = {}

        if deployment_id || public_network || private_network
            missing = []

            missing << '--opennebula-cluster' if deployment_id.nil?
            missing << '--public-network'    if public_network.nil?
            missing << '--private-network'   if private_network.nil?

            unless missing.empty?
                STDERR.puts(
                    "#{missing.join(', ')} must be provided together."
                )

                exit(-1)
            end

            body = {
                :cluster  => { :id => deployment_id },
                :networks => {
                    :public  => { :id => public_network },
                    :private => { :id => private_network }
                }
            }
        end

        client = OneKSHelper.client(options)

        if resource
            unless resource == 'cluster'
                STDERR.puts(
                    "Check operation is not supported for #{resource}. " \
                    "Use 'oneks check cluster <cluster_id>' instead."
                )
                exit(-1)
            end

            if resource_id.nil?
                STDERR.puts 'A cluster ID is required'
                STDERR.puts 'Usage: oneks check cluster <cluster_id>'
                exit(-1)
            end

            if deployment_id || public_network || private_network
                STDERR.puts 'Do not provide deployment options when using a cluster ID, ' \
                            'the deployment defined by the cluster is used automatically.'
                exit(-1)
            end

            body = { :cluster_id => resource_id }
        end

        kscluster.check(client, body)
    rescue Interrupt
        puts 'Aborting...'
        exit(1)
    end

    command :delete,
            delete_desc,
            :resource,
            :resource_id,
            :options => [OpenNebulaHelper::FORCE, CLUSTER_ID] do
        resource   = args[0]
        identifier = args[1]

        if resource != 'app' && !identifier.to_s.match?(/^\d+$/)
            STDERR.puts "Argument '#{identifier}' is not a valid ID"
            exit(-1)
        end

        if options[:force] && resource != 'cluster'
            STDERR.puts '--force is only supported for cluster deletion'
            exit(-1)
        end

        if resource == 'app' && options[:cluster_id].nil?
            STDERR.puts 'A cluster ID is required for Application deletion'
            STDERR.puts 'Usage: oneks delete app <release_name> --cluster-id <cluster_id>'
            exit(-1)
        end

        client = OneKSHelper.client(options)
        rc =
            case resource
            when 'cluster'
                kscluster.delete(client, [identifier.to_i], options)
            when 'group'
                ksgroup.delete(client, [identifier.to_i])
            when 'app'
                ksapp.delete(client, options[:cluster_id], identifier)
            end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        rc
    rescue Interrupt
        puts
        exit(1)
    end

    logs_desc = <<-EOT.unindent
        Show logs for a OneKS cluster by ID.
        Supported resources: cluster.

        Examples:
            # Show cluster logs
            oneks logs cluster <cluster_id>
    EOT

    command :logs,
            logs_desc,
            :resource,
            :resource_id,
            :options => [ALL_LOGS, FOLLOW_LOGS] do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]

        case resource
        when 'cluster'
            rc = kscluster.logs(client, resource_id, options)
        when 'group', 'app'
            STDERR.puts(
                "Logs operation is not supported for #{resource}. " \
                "Use 'oneks logs cluster <cluster_id>' instead."
            )
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end

    upgrade_desc = <<-EOT.unindent
        Upgrade a OneKS cluster by ID.
        Supported resources: cluster.

        Examples:
            # Upgrade a cluster
            oneks upgrade cluster <cluster_id>
    EOT

    command :upgrade,
            upgrade_desc,
            :resource,
            :resource_id,
            :options => [KUBERNETES_VERSION] do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]

        case resource
        when 'cluster'
            rc = kscluster.upgrade(client, resource_id, options)
        when 'group', 'app'
            STDERR.puts(
                "Upgrade operation is not supported for #{resource}. " \
                'Only clusters can be upgraded.'
            )
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end

    scale_desc = <<-EOT.unindent
        Scale a OneKS group to a desired target size.
        Supported resources: group.

        Examples:
            # Scale a group to 2 nodes
            oneks scale group <group_id> --target 2
    EOT

    command :scale,
            scale_desc,
            :resource,
            :resource_id,
            :options => [TARGET] do
        client      = OneKSHelper.client(options)
        resource    = args[0]
        resource_id = args[1]
        target      = options[:target]

        case resource
        when 'cluster'
            STDERR.puts(
                "Scale operation is not supported for #{resource}.\n" \
                'To increase cluster capacity, create a new group using: ' \
                "'oneks create group --cluster-id <cluster_id>'."
            )
            exit(-1)
        when 'group'
            rc = ksgroup.scale(client, resource_id, target)
        when 'app'
            STDERR.puts('Scale is only supported for groups')
            exit(-1)
        end

        return [rc[:err_code], rc[:message]] if CloudClient.is_error?(rc)

        0
    rescue Interrupt
        puts
        exit(1)
    end
end
