kubernetes.rb 6.4 KB
Newer Older
1 2 3
module Clusters
  module Platforms
    class Kubernetes < ActiveRecord::Base
4 5 6 7
      include Gitlab::CurrentSettings
      include Gitlab::Kubernetes
      include ReactiveCaching

S
Shinya Maeda 已提交
8
      self.table_name = 'cluster_platforms_kubernetes'
9
      self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] }
10

S
Shinya Maeda 已提交
11
      belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
12 13 14 15 16 17 18 19 20 21 22

      attr_encrypted :password,
        mode: :per_attribute_iv,
        key: Gitlab::Application.secrets.db_key_base,
        algorithm: 'aes-256-cbc'

      attr_encrypted :token,
        mode: :per_attribute_iv,
        key: Gitlab::Application.secrets.db_key_base,
        algorithm: 'aes-256-cbc'

S
Shinya Maeda 已提交
23 24
      before_validation :enforce_namespace_to_lower_case

25 26 27 28 29 30 31 32
      validates :namespace,
        allow_blank: true,
        length: 1..63,
        format: {
          with: Gitlab::Regex.kubernetes_namespace_regex,
          message: Gitlab::Regex.kubernetes_namespace_regex_message
        }

33
      # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
34 35
      validates :api_url, url: true, presence: true
      validates :token, presence: true
36

37 38
      after_save :clear_reactive_cache!

39 40
      # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
      after_destroy :destroy_kubernetes_integration!
S
Shinya Maeda 已提交
41 42 43 44 45 46

      alias_attribute :ca_pem, :ca_cert

      delegate :project, to: :cluster, allow_nil: true
      delegate :enabled?, to: :cluster, allow_nil: true

47 48 49 50 51 52 53 54
      def actual_namespace
        if namespace.present?
          namespace
        else
          default_namespace
        end
      end

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
      def predefined_variables
        config = YAML.dump(kubeconfig)

        variables = [
          { key: 'KUBE_URL', value: api_url, public: true },
          { key: 'KUBE_TOKEN', value: token, public: false },
          { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true },
          { key: 'KUBECONFIG', value: config, public: false, file: true }
        ]

        if ca_pem.present?
          variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
          variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
        end

        variables
      end

      # Constructs a list of terminals from the reactive cache
      #
      # Returns nil if the cache is empty, in which case you should try again a
      # short time later
      def terminals(environment)
        with_reactive_cache do |data|
          pods = filter_by_label(data[:pods], app: environment.slug)
          terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }
          terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
        end
      end

      # Caches resources in the namespace so other calls don't need to block on
      # network access
      def calculate_reactive_cache
        return unless active? && project && !project.pending_delete?

        # We may want to cache extra things in the future
        { pods: read_pods }
      end

94 95 96 97
      def kubeclient
        @kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service?
      end

98 99 100
      def update_kubernetes_integration!
        raise 'Kubernetes service already configured' unless manages_kubernetes_service?

101 102 103
        # This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
        cluster.reload

S
Shinya Maeda 已提交
104
        ensure_kubernetes_service&.update!(
105 106 107 108 109 110 111 112
          active: enabled?,
          api_url: api_url,
          namespace: namespace,
          token: token,
          ca_pem: ca_cert
        )
      end

113 114 115 116
      def active?
        manages_kubernetes_service?
      end

117
      private
118

119 120 121 122 123 124
      def kubeconfig
        to_kubeconfig(
          url: api_url,
          namespace: actual_namespace,
          token: token,
          ca_pem: ca_pem)
125 126
      end

127 128
      def default_namespace
        return unless project
129

130 131
        slug = "#{project.path}-#{project.id}".downcase
        slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
132
      end
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149

      def build_kubeclient!(api_path: 'api', api_version: 'v1')
        raise "Incomplete settings" unless api_url && actual_namespace

        unless (username && password) || token
          raise "Either username/password or token is required to access API"
        end

        ::Kubeclient::Client.new(
          join_api_url(api_path),
          api_version,
          auth_options: kubeclient_auth_options,
          ssl_options: kubeclient_ssl_options,
          http_proxy_uri: ENV['http_proxy']
        )
      end

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
      # Returns a hash of all pods in the namespace
      def read_pods
        kubeclient = build_kubeclient!

        kubeclient.get_pods(namespace: actual_namespace).as_json
      rescue KubeException => err
        raise err unless err.error_code == 404
        []
      end

      def kubeclient_ssl_options
        opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }

        if ca_pem.present?
          opts[:cert_store] = OpenSSL::X509::Store.new
          opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
        end

        opts
      end

171
      def kubeclient_auth_options
172
        { bearer_token: token }
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
      end

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

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

        url.to_s
      end

      def terminal_auth
        {
          token: token,
          ca_pem: ca_pem,
          max_session_time: current_application_settings.terminal_max_session_time
        }
      end

      def enforce_namespace_to_lower_case
        self.namespace = self.namespace&.downcase
      end
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219

      def enforce_namespace_to_lower_case
        self.namespace = self.namespace&.downcase
      end

      # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
      def manages_kubernetes_service?
        return true unless kubernetes_service&.active?

        kubernetes_service.api_url == api_url
      end

      def destroy_kubernetes_integration!
        return unless manages_kubernetes_service?

        kubernetes_service&.destroy!
      end

      def kubernetes_service
        @kubernetes_service ||= project&.kubernetes_service
      end

      def ensure_kubernetes_service
        @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
      end
220 221 222
    end
  end
end