# frozen_string_literal: true

require 'uri'

module Gitlab
  module Kubernetes
    # Wrapper around Kubeclient::Client to dispatch
    # the right message to the client that can respond to the message.
    # We must have a kubeclient for each ApiGroup as there is no
    # other way to use the Kubeclient gem.
    #
    # See https://github.com/abonas/kubeclient/issues/348.
    class KubeClient
      include Gitlab::Utils::StrongMemoize

      SUPPORTED_API_GROUPS = {
        core: { group: 'api', version: 'v1' },
        rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' },
        apps: { group: 'apis/apps', version: 'v1' },
        extensions: { group: 'apis/extensions', version: 'v1beta1' },
        istio: { group: 'apis/networking.istio.io', version: 'v1alpha3' },
        knative: { group: 'apis/serving.knative.dev', version: 'v1alpha1' },
        metrics: { group: 'apis/metrics.k8s.io', version: 'v1beta1' },
        networking: { group: 'apis/networking.k8s.io', version: 'v1' },
        cilium_networking: { group: 'apis/cilium.io', version: 'v2' }
      }.freeze

      SUPPORTED_API_GROUPS.each do |name, params|
        client_method_name = "#{name}_client".to_sym

        define_method(client_method_name) do
          strong_memoize(client_method_name) do
            build_kubeclient(params[:group], params[:version])
          end
        end
      end

      # Core API methods delegates to the core api group client
      delegate :get_nodes,
        :get_pods,
        :get_secrets,
        :get_config_map,
        :get_namespace,
        :get_pod,
        :get_secret,
        :get_service,
        :get_service_account,
        :delete_namespace,
        :delete_pod,
        :delete_service_account,
        :create_config_map,
        :create_namespace,
        :create_pod,
        :create_secret,
        :create_service_account,
        :update_config_map,
        :update_secret,
        :update_service_account,
        to: :core_client

      # RBAC methods delegates to the apis/rbac.authorization.k8s.io api
      # group client
      delegate :update_cluster_role_binding,
        :create_role,
        :get_role,
        :update_role,
        :delete_role_binding,
        :update_role_binding,
        to: :rbac_client

      # non-entity methods that can only work with the core client
      # as it uses the pods/log resource
      delegate :get_pod_log,
        :watch_pod_log,
        to: :core_client

      # Gateway methods delegate to the apis/networking.istio.io api
      # group client
      delegate :create_gateway,
        :get_gateway,
        :update_gateway,
        to: :istio_client

      # NetworkPolicy methods delegate to the apis/networking.k8s.io api
      # group client
      delegate :create_network_policy,
        :get_network_policies,
        :get_network_policy,
        :update_network_policy,
        :delete_network_policy,
        to: :networking_client

      # CiliumNetworkPolicy methods delegate to the apis/cilium.io api
      # group client
      delegate :create_cilium_network_policy,
        :get_cilium_network_policies,
        :get_cilium_network_policy,
        :update_cilium_network_policy,
        :delete_cilium_network_policy,
        to: :cilium_networking_client

      attr_reader :api_prefix, :kubeclient_options

      DEFAULT_KUBECLIENT_OPTIONS = {
        timeouts: {
          open: 10,
          read: 30
        }
      }.freeze

      def self.graceful_request(cluster_id)
        { status: :connected, response: yield }
      rescue *Gitlab::Kubernetes::Errors::CONNECTION
        { status: :unreachable, connection_error: :connection_error }
      rescue *Gitlab::Kubernetes::Errors::AUTHENTICATION
        { status: :authentication_failure, connection_error: :authentication_error }
      rescue Kubeclient::HttpError => e
        { status: kubeclient_error_status(e.message), connection_error: :http_error }
      rescue StandardError => e
        Gitlab::ErrorTracking.track_exception(e, cluster_id: cluster_id)

        { status: :unknown_failure, connection_error: :unknown_error }
      end

      # KubeClient uses the same error class
      # For connection errors (eg. timeout) and
      # for Kubernetes errors.
      def self.kubeclient_error_status(message)
        if message&.match?(/timed out|timeout/i)
          :unreachable
        else
          :authentication_failure
        end
      end

      # We disable redirects through 'http_max_redirects: 0',
      # so that KubeClient does not follow redirects and
      # expose internal services.
      def initialize(api_prefix, **kubeclient_options)
        @api_prefix = api_prefix
        @kubeclient_options = DEFAULT_KUBECLIENT_OPTIONS
          .deep_merge(kubeclient_options)
          .merge(http_max_redirects: 0)

        validate_url!
      end

      # Deployments resource is currently on the apis/extensions api group
      # until Kubernetes 1.15. Kubernetest 1.16+ has deployments resources in
      # the apis/apps api group.
      #
      # As we still support Kubernetes 1.12+, we will need to support both.
      def get_deployments(**args)
        extensions_client.discover unless extensions_client.discovered

        if extensions_client.respond_to?(:get_deployments)
          extensions_client.get_deployments(**args)
        else
          apps_client.get_deployments(**args)
        end
      end

      # Ingresses resource is currently on the apis/extensions api group
      # until Kubernetes 1.21. Kubernetest 1.22+ has ingresses resources in
      # the networking.k8s.io/v1 api group.
      #
      # As we still support Kubernetes 1.12+, we will need to support both.
      def get_ingresses(**args)
        extensions_client.discover unless extensions_client.discovered

        if extensions_client.respond_to?(:get_ingresses)
          extensions_client.get_ingresses(**args)
        else
          networking_client.get_ingresses(**args)
        end
      end

      def patch_ingress(*args)
        extensions_client.discover unless extensions_client.discovered

        if extensions_client.respond_to?(:patch_ingress)
          extensions_client.patch_ingress(*args)
        else
          networking_client.patch_ingress(*args)
        end
      end

      def create_or_update_cluster_role_binding(resource)
        update_cluster_role_binding(resource)
      end

      # Note that we cannot update roleRef as that is immutable
      def create_or_update_role_binding(resource)
        update_role_binding(resource)
      end

      def create_or_update_service_account(resource)
        if service_account_exists?(resource)
          update_service_account(resource)
        else
          create_service_account(resource)
        end
      end

      def create_or_update_secret(resource)
        if secret_exists?(resource)
          update_secret(resource)
        else
          create_secret(resource)
        end
      end

      private

      def validate_url!
        return if Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?

        Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false)
      end

      def service_account_exists?(resource)
        get_service_account(resource.metadata.name, resource.metadata.namespace)
      rescue ::Kubeclient::ResourceNotFoundError
        false
      end

      def secret_exists?(resource)
        get_secret(resource.metadata.name, resource.metadata.namespace)
      rescue ::Kubeclient::ResourceNotFoundError
        false
      end

      def build_kubeclient(api_group, api_version)
        ::Kubeclient::Client.new(
          join_api_url(api_prefix, api_group),
          api_version,
          **kubeclient_options
        )
      end

      def join_api_url(api_prefix, api_path)
        url = URI.parse(api_prefix)
        prefix = url.path.sub(%r{/+\z}, '')

        url.path = [prefix, api_path].join("/")

        url.to_s
      end
    end
  end
end
