lib/elastic_apm/metadata/system_info/container_info.rb



# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you 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.

# frozen_string_literal: true

module ElasticAPM
  class Metadata
    class SystemInfo
      # @api private
      class ContainerInfo
        CGROUP_PATH = '/proc/self/cgroup'

        attr_accessor :container_id, :kubernetes_namespace,
          :kubernetes_node_name, :kubernetes_pod_name, :kubernetes_pod_uid

        def initialize(cgroup_path: CGROUP_PATH)
          @cgroup_path = cgroup_path
        end

        attr_reader :cgroup_path

        def read!(hostname)
          read_from_cgroup!
          self.kubernetes_pod_name = hostname if kubernetes_pod_uid
          read_from_env!
          self
        end

        def self.read!(hostname)
          new.read!(hostname)
        end

        def container
          @container ||=
            begin
              return unless container_id
              { id: container_id }
            end
        end

        def kubernetes
          @kubernetes =
            begin
              kubernetes = {
                namespace: kubernetes_namespace,
                node: { name: kubernetes_node_name },
                pod: {
                  name: kubernetes_pod_name,
                  uid: kubernetes_pod_uid
                }
              }
              return nil if kubernetes.values.all?(&:nil?)

              kubernetes
            end
        end

        private

        def read_from_env!
          self.kubernetes_namespace =
            ENV.fetch('KUBERNETES_NAMESPACE', kubernetes_namespace)
          self.kubernetes_node_name =
            ENV.fetch('KUBERNETES_NODE_NAME', kubernetes_node_name)
          self.kubernetes_pod_name =
            ENV.fetch('KUBERNETES_POD_NAME', kubernetes_pod_name)
          self.kubernetes_pod_uid =
            ENV.fetch('KUBERNETES_POD_UID', kubernetes_pod_uid)
        end

        # rubocop:disable Style/RegexpLiteral
        CONTAINER_ID_REGEXES = [
          %r{^[[:xdigit:]]{64}$},
          %r{
            ^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]
            {4}-[[:xdigit:]]{4}-[[:xdigit:]]{4,}$
          }x
        ].freeze
        KUBEPODS_REGEXES = [
          %r{(?:^/kubepods[^\s]*/pod([^/]+)$)},
          %r{
            (?:^/kubepods\.slice/(kubepods-[^/]+\.slice/)?
             kubepods[^/]*-pod([^/]+)\.slice$)
          }x
        ].freeze
        SYSTEMD_SCOPE_SUFFIX = '.scope'
        # rubocop:enable Style/RegexpLiteral

        # rubocop:disable Metrics/PerceivedComplexity
        # rubocop:disable Metrics/CyclomaticComplexity
        def read_from_cgroup!
          return unless File.exist?(cgroup_path)
          IO.readlines(cgroup_path).each do |line|
            parts = line.strip.split(':')
            next if parts.length != 3

            cgroup_path = parts[2]

            # Depending on the filesystem driver used for cgroup
            # management, the paths in /proc/pid/cgroup will have
            # one of the following formats in a Docker container:
            #
            #   systemd: /system.slice/docker-<container-ID>.scope
            #   cgroupfs: /docker/<container-ID>
            #
            # In a Kubernetes pod, the cgroup path will look like:
            #
            #   systemd:
            #      /kubepods.slice/kubepods-<QoS-class>.slice/kubepods-\
            #        <QoS-class>-pod<pod-UID>.slice/<container-iD>.scope
            #   cgroupfs:
            #      /kubepods/<QoS-class>/pod<pod-UID>/<container-iD>
            directory, container_id = File.split(cgroup_path)

            if container_id.end_with?(SYSTEMD_SCOPE_SUFFIX)
              container_id = container_id[0...-SYSTEMD_SCOPE_SUFFIX.length]
              if container_id.include?('-')
                container_id = container_id.split('-', 2)[1]
              end
            end

            if (kubepods_match = match_kubepods(directory))
              unless (pod_id = kubepods_match[1])
                pod_id = kubepods_match[2]
                pod_id&.tr!('_', '-')
              end

              self.container_id = container_id
              self.kubernetes_pod_uid = pod_id
            elsif match_container(container_id)
              self.container_id = container_id
            end
          end
        end
        # rubocop:enable Metrics/PerceivedComplexity
        # rubocop:enable Metrics/CyclomaticComplexity

        def match_kubepods(directory)
          KUBEPODS_REGEXES.each do |r|
            next unless (match = r.match(directory))
            return match
          end

          nil
        end

        def match_container(container_id)
          CONTAINER_ID_REGEXES.each do |r|
            next unless (match = r.match(container_id))
            return match
          end

          nil
        end
      end
    end
  end
end