Răsfoiți Sursa

squashing commits before publishing

Anton Petruhin 4 ani în urmă
comite
92ef04b486
90 a modificat fișierele cu 7207 adăugiri și 0 ștergeri
  1. 47 0
      .github/workflows/docker-publish.yml
  2. 2 0
      .gitignore
  3. 14 0
      Dockerfile
  4. 201 0
      LICENSE
  5. 193 0
      README.md
  6. 80 0
      cgroup/blkio.go
  7. 24 0
      cgroup/blkio_test.go
  8. 211 0
      cgroup/cgroup.go
  9. 91 0
      cgroup/cgroup_test.go
  10. 42 0
      cgroup/cpu.go
  11. 39 0
      cgroup/cpu_test.go
  12. 26 0
      cgroup/fixtures/cgroup/blkio/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/blkio.throttle.io_service_bytes
  13. 26 0
      cgroup/fixtures/cgroup/blkio/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/blkio.throttle.io_serviced
  14. 2 0
      cgroup/fixtures/cgroup/cpu/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/cpu.cfs_period_us
  15. 2 0
      cgroup/fixtures/cgroup/cpu/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/cpu.cfs_quota_us
  16. 3 0
      cgroup/fixtures/cgroup/cpu/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/cpu.stat
  17. 1 0
      cgroup/fixtures/cgroup/cpu/system.slice/ssh.service/cpu.cfs_period_us
  18. 1 0
      cgroup/fixtures/cgroup/cpu/system.slice/ssh.service/cpu.cfs_quota_us
  19. 3 0
      cgroup/fixtures/cgroup/cpu/system.slice/ssh.service/cpu.stat
  20. 1 0
      cgroup/fixtures/cgroup/cpuacct/system.slice/ssh.service/cpuacct.usage
  21. 1 0
      cgroup/fixtures/cgroup/memory/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/memory.limit_in_bytes
  22. 32 0
      cgroup/fixtures/cgroup/memory/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/memory.stat
  23. 1 0
      cgroup/fixtures/cgroup/memory/system.slice/ssh.service/memory.limit_in_bytes
  24. 11 0
      cgroup/fixtures/proc/100/cgroup
  25. 11 0
      cgroup/fixtures/proc/200/cgroup
  26. 11 0
      cgroup/fixtures/proc/300/cgroup
  27. 42 0
      cgroup/memory.go
  28. 32 0
      cgroup/memory_test.go
  29. 44 0
      cgroup/utils.go
  30. 7 0
      common/error.go
  31. 11 0
      common/kernel.go
  32. 16 0
      common/net.go
  33. 80 0
      containers/app.go
  34. 60 0
      containers/conntrack.go
  35. 742 0
      containers/container.go
  36. 77 0
      containers/containerd.go
  37. 81 0
      containers/dockerd.go
  38. 43 0
      containers/journald.go
  39. 85 0
      containers/metrics.go
  40. 299 0
      containers/registry.go
  41. 47 0
      containers/taskstats.go
  42. 20 0
      containers/volumes.go
  43. 16 0
      ebpftracer/Dockerfile
  44. 31 0
      ebpftracer/Makefile
  45. 31 0
      ebpftracer/Vagrantfile
  46. 3 0
      ebpftracer/ebpf.go
  47. 23 0
      ebpftracer/ebpf/ebpf.c
  48. 100 0
      ebpftracer/ebpf/file.c
  49. 80 0
      ebpftracer/ebpf/proc.c
  50. 36 0
      ebpftracer/ebpf/tcp_retransmit.c
  51. 118 0
      ebpftracer/ebpf/tcp_state.c
  52. 70 0
      ebpftracer/init.go
  53. 265 0
      ebpftracer/tracer.go
  54. 336 0
      ebpftracer/tracer_test.go
  55. 46 0
      flags/flags.go
  56. 37 0
      go.mod
  57. 1060 0
      go.sum
  58. 107 0
      logs/journald_reader.go
  59. 153 0
      logs/tail_reader.go
  60. 76 0
      logs/tail_reader_test.go
  61. 125 0
      main.go
  62. 52 0
      manifests/coroot-node-agent.yaml
  63. 266 0
      node/collector.go
  64. 63 0
      node/cpu.go
  65. 28 0
      node/cpu_test.go
  66. 107 0
      node/disk.go
  67. 53 0
      node/disk_test.go
  68. 25 0
      node/fixtures/diskstats
  69. 46 0
      node/fixtures/proc/meminfo
  70. 24 0
      node/fixtures/proc/stat
  71. 43 0
      node/memory.go
  72. 20 0
      node/memory_test.go
  73. 56 0
      node/metadata/aws.go
  74. 82 0
      node/metadata/azure.go
  75. 49 0
      node/metadata/gcp.go
  76. 65 0
      node/metadata/metadata.go
  77. 69 0
      node/net.go
  78. 254 0
      pinger/pinger.go
  79. 38 0
      proc/fd.go
  80. 1 0
      proc/fixtures/123/fd/4
  81. 3 0
      proc/fixtures/123/fdinfo/4
  82. 39 0
      proc/fixtures/123/mountinfo
  83. 3 0
      proc/fixtures/123/net/tcp
  84. 2 0
      proc/fixtures/123/net/tcp6
  85. 23 0
      proc/fs.go
  86. 30 0
      proc/mount.go
  87. 108 0
      proc/net.go
  88. 71 0
      proc/ns.go
  89. 55 0
      proc/proc.go
  90. 57 0
      proc/proc_test.go

+ 47 - 0
.github/workflows/docker-publish.yml

@@ -0,0 +1,47 @@
+name: Docker Image
+
+on:
+  release:
+    types: [created]
+
+env:
+  REGISTRY: ghcr.io
+  IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+
+      - name: Log into registry ${{ env.REGISTRY }}
+        uses: docker/login-action@v1
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Extract Docker metadata
+        id: meta
+        uses: docker/metadata-action@v3
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+          tags: |
+            type=ref,event=branch
+            type=ref,event=pr
+            type=semver,pattern={{version}}
+
+      - name: Build and push Docker image
+        uses: docker/build-push-action@v2
+        with:
+          context: .
+          push: true
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          build-args: |
+            VERSION=${{ steps.meta.outputs.version }}

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+.idea
+.vagrant

+ 14 - 0
Dockerfile

@@ -0,0 +1,14 @@
+FROM golang:1.16-buster AS builder
+RUN apt-get update && apt-get install -y libsystemd-dev
+COPY go.mod /tmp/src/
+COPY go.sum /tmp/src/
+WORKDIR /tmp/src/
+RUN go mod download
+COPY . /tmp/src/
+ARG VERSION=unknown
+RUN CGO_ENABLED=1 go install -mod=readonly -ldflags "-X main.version=$VERSION" /tmp/src
+
+FROM debian:buster
+RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
+COPY --from=builder /go/bin/coroot-node-agent /usr/bin/coroot-node-agent
+ENTRYPOINT ["coroot-node-agent"]

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2020-present Coroot, Inc.
+
+   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.

+ 193 - 0
README.md

@@ -0,0 +1,193 @@
+# Coroot-node-agent
+
+The agent gathers metrics related to a node and the containers running on it, and it exposes them in the Prometheus format.
+
+It uses eBPF to track container related events such as TCP connects, so the minimum supported Linux kernel version is 4.16.
+
+## Features
+
+### TCP connection tracing
+
+To provide visibility into the relationships between services, the agent traces containers TCP events, such as *connect()* and *listen()*.
+
+Exported metrics are useful for:
+* Obtaining an actual map of inter-service communications. It doesn't require integration of distributed tracing frameworks into your code.
+* Detecting connections errors from one service to another.
+* Measuring network latency between containers, nodes and availability zones.
+
+### Log patterns extraction
+
+Log management is usually quite expensive. In most cases, you do not need to analyze each event individually.
+It is enough to extract recurring patterns and the number of the related events.
+
+This approach drastically reduces the amount of data required for express log analysis.
+
+The agent discovers container logs and parses them right on the node.
+
+At the moment the following sources are supported:
+* Direct logging to files in */var/log/*
+* Journald
+* Dockerd (JSON file driver)
+* Containerd (CRI logs)
+
+### Delay accounting
+
+[Delay accounting](https://www.kernel.org/doc/html/latest/accounting/delay-accounting.html) allows engineers to accurately
+identify situations where a container is experiencing a lack of CPU time or waiting for I/O.
+
+The agent gathers per-process counters through [Netlink](https://man7.org/linux/man-pages/man7/netlink.7.html) and aggregates them into per-container metrics:
+* [container_resources_cpu_delay_seconds_total](https://coroot.com/docs/metrics/node-agent#container_resources_cpu_delay_seconds_total)
+* [container_resources_disk_delay_seconds_total](https://coroot.com/docs/metrics/node-agent#container_resources_disk_delay_seconds_total)
+
+### Out-of-memory events tracing
+
+The [container_oom_kills_total](https://coroot.com/docs/metrics/node-agent#container_oom_kills_total) metric shows that a container has been terminated by the OOM killer.
+
+### Instance meta information
+
+If a node is a cloud instance, the agent identifies a cloud provider and collects additional information using the related metadata services.
+
+Supported cloud providers: [AWS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html), [GCP](https://cloud.google.com/compute/docs/metadata/overview), [Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/instance-metadata-service?tabs=linux)
+
+Collected info:
+* AccountID
+* InstanceID
+* Instance/machine type
+* Region
+* AvailabilityZone + AvailabilityZoneId (AWS only)
+* LifeCycle: on-demand/spot (AWS and GCP only)
+* Private & Public IP addresses
+
+## Run
+
+### Requirements
+
+The agent requires some privileges for getting access to container data, such as logs, performance counters and TCP sockets:
+* privileged mode (`securityContext.privileged: true`)
+* the host process ID namespace (`hostPID: true`)
+* `/sys/fs/cgroup` and `/sys/kernel/debug` should be mounted to the agent's container
+
+### Kubernetes
+
+```yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: coroot
+
+---
+
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  labels:
+    app: coroot-node-agent
+  name: coroot-node-agent
+  namespace: coroot
+spec:
+  selector:
+    matchLabels:
+      app: coroot-node-agent
+  template:
+    metadata:
+      labels:
+        app: coroot-node-agent
+      annotations:
+        prometheus.io/scrape: 'true'
+        prometheus.io/port: '80'
+    spec:
+      tolerations:
+        - operator: Exists
+      hostPID: true
+      containers:
+        - name: coroot-node-agent
+          image: ghcr.io/coroot/coroot-node-agent:latest
+          args: ["--cgroupfs-root", "/host/sys/fs/cgroup"]
+          ports:
+            - containerPort: 80
+              name: http
+          securityContext:
+            privileged: true
+          volumeMounts:
+            - mountPath: /host/sys/fs/cgroup
+              name: cgroupfs
+              readOnly: true
+            - mountPath: /sys/kernel/debug
+              name: debugfs
+              readOnly: false
+      volumes:
+        - hostPath:
+            path: /sys/fs/cgroup
+          name: cgroupfs
+        - hostPath:
+            path: /sys/kernel/debug
+          name: debugfs
+```
+
+If you use [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator), 
+you will also need to create a PodMonitor:
+```yaml
+apiVersion: monitoring.coreos.com/v1
+kind: PodMonitor
+metadata:
+  name: coroot-node-agent
+  namespace: coroot
+spec:
+  selector:
+    matchLabels:
+      app: coroot-node-agent
+  podMetricsEndpoints:
+    - port: http
+```
+
+Make sure the PodMonitor matches `podMonitorSelector` defined in your Prometheus:
+```yaml
+apiVersion: monitoring.coreos.com/v1
+kind: Prometheus
+...
+spec:
+  ...
+  podMonitorNamespaceSelector: {}
+  podMonitorSelector: {}
+  ...
+```
+The special value `{}` allows Prometheus to watch all the PodMonitors from all namespaces. 
+
+### Docker
+
+```bash
+docker run --detach --name coroot-node-agent \
+    --privileged --pid host \
+    -v /sys/kernel/debug:/sys/kernel/debug:rw \
+    -v /sys/fs/cgroup:/host/sys/fs/cgroup:ro \
+    ghcr.io/coroot/coroot-node-agent --cgroupfs-root=/host/sys/fs/cgroup
+```
+
+### Flags
+
+```bash
+usage: coroot-node-agent [<flags>]
+
+Flags:
+      --listen="0.0.0.0:80"  Listen address - ip:port or :port
+      --cgroupfs-root="/sys/fs/cgroup"
+                             The mount point of the host cgroupfs root
+      --no-parse-logs        Disable container logs parsing
+      --no-ping-upstreams    Disable container upstreams ping
+      --track-public-network=TRACK-PUBLIC-NETWORK ...
+                             Allow track connections to the specified IP networks, all private networks are allowed by default (e.g., Y.Y.Y.Y/mask)
+      --provider=PROVIDER    `provider` label for `node_cloud_info` metric
+      --region=REGION        `region` label for `node_cloud_info` metric
+      --availability-zone=AVAILABILITY-ZONE
+                             `availability_zone` label for `node_cloud_info` metric
+```
+
+## Metrics
+
+The collected metrics are described [here](https://coroot.com/docs/metrics/node-agent).
+
+## License
+
+Coroot-node-agent is licensed under the [Apache License, Version 2.0](https://github.com/coroot/coroot-node-agent/blob/main/LICENSE).
+
+The BPF code is licensed under the General Public License, Version 2.0.

+ 80 - 0
cgroup/blkio.go

@@ -0,0 +1,80 @@
+package cgroup
+
+import (
+	"io/ioutil"
+	"k8s.io/klog/v2"
+	"path"
+	"strconv"
+	"strings"
+)
+
+type BlkioStat struct {
+	ReadOps      uint64
+	WriteOps     uint64
+	ReadBytes    uint64
+	WrittenBytes uint64
+}
+
+func (cg *Cgroup) BlkioStat() (map[string]BlkioStat, error) {
+	ops, err := readBlkioStatFile(path.Join(cgRoot, "blkio", cg.subsystems["blkio"], "blkio.throttle.io_serviced"))
+	if err != nil {
+		return nil, err
+	}
+	bytes, err := readBlkioStatFile(path.Join(cgRoot, "blkio", cg.subsystems["blkio"], "blkio.throttle.io_service_bytes"))
+	if err != nil {
+		return nil, err
+	}
+	res := map[string]BlkioStat{}
+	for _, v := range ops {
+		stat := res[v.majorMinor]
+		switch v.name {
+		case "Read":
+			stat.ReadOps = v.value
+		case "Write":
+			stat.WriteOps = v.value
+		}
+		res[v.majorMinor] = stat
+	}
+	for _, v := range bytes {
+		stat := res[v.majorMinor]
+		switch v.name {
+		case "Read":
+			stat.ReadBytes = v.value
+		case "Write":
+			stat.WrittenBytes = v.value
+		}
+		res[v.majorMinor] = stat
+	}
+	return res, nil
+}
+
+type blkioVariable struct {
+	majorMinor string
+	name       string
+	value      uint64
+}
+
+func readBlkioStatFile(filePath string) ([]blkioVariable, error) {
+	data, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return nil, err
+	}
+	var res []blkioVariable
+	for _, line := range strings.Split(string(data), "\n") {
+		parts := strings.Fields(line)
+		if len(parts) != 3 {
+			continue
+		}
+		v, err := strconv.ParseUint(parts[2], 10, 64)
+		if err != nil {
+			klog.Warningf(`failed to parse blkio stat line "%s": %s`, line, err)
+			continue
+		}
+		res = append(res, blkioVariable{
+			majorMinor: parts[0],
+			name:       parts[1],
+			value:      v,
+		})
+	}
+	return res, nil
+}

+ 24 - 0
cgroup/blkio_test.go

@@ -0,0 +1,24 @@
+package cgroup
+
+import (
+	"github.com/stretchr/testify/assert"
+	"path"
+	"testing"
+)
+
+func TestCgroup_BlkioStat(t *testing.T) {
+	cgRoot = "fixtures/cgroup"
+
+	cg, _ := NewFromProcessCgroupFile(path.Join("fixtures/proc/200/cgroup"))
+	stat, err := cg.BlkioStat()
+	assert.Nil(t, err)
+	assert.Equal(t,
+		map[string]BlkioStat{
+			"8:0":  {ReadOps: 0, WriteOps: 281, ReadBytes: 0, WrittenBytes: 4603904},
+			"8:16": {ReadOps: 0, WriteOps: 39, ReadBytes: 0, WrittenBytes: 655360},
+			"8:32": {ReadOps: 23043666, WriteOps: 28906992, ReadBytes: 998632854016, WrittenBytes: 884175858688},
+			"8:48": {ReadOps: 20689345, WriteOps: 27906791, ReadBytes: 875529547776, WrittenBytes: 753046432768},
+			"9:1":  {ReadOps: 633949, WriteOps: 4, ReadBytes: 10238894080, WrittenBytes: 49152},
+		},
+		stat)
+}

+ 211 - 0
cgroup/cgroup.go

@@ -0,0 +1,211 @@
+package cgroup
+
+import (
+	"fmt"
+	"github.com/coroot/coroot-node-agent/common"
+	"github.com/coroot/coroot-node-agent/flags"
+	"io/ioutil"
+	"k8s.io/klog/v2"
+	"os"
+	"path"
+	"regexp"
+	"strings"
+	"time"
+)
+
+var (
+	cgRoot = *flags.CgroupRoot
+
+	dockerIdRegexp      = regexp.MustCompile(`([a-z0-9]{64})`)
+	crioIdRegexp        = regexp.MustCompile(`crio-([a-z0-9]{64})`)
+	lxcIdRegexp         = regexp.MustCompile(`/lxc/([^/]+)`)
+	systemSliceIdRegexp = regexp.MustCompile(`(/system\.slice/([^/]+))`)
+)
+
+type ContainerType uint8
+
+const (
+	ContainerTypeUnknown ContainerType = iota
+	ContainerTypeStandaloneProcess
+	ContainerTypeDocker
+	ContainerTypeCrio
+	ContainerTypeLxc
+	ContainerTypeSystemdService
+)
+
+func (t ContainerType) String() string {
+	switch t {
+	case ContainerTypeStandaloneProcess:
+		return "standalone"
+	case ContainerTypeDocker:
+		return "docker"
+	case ContainerTypeCrio:
+		return "crio"
+	case ContainerTypeLxc:
+		return "lxc"
+	case ContainerTypeSystemdService:
+		return "systemd"
+	default:
+		return "unknown"
+	}
+}
+
+type Stats struct {
+	CpuUsageSeconds      float64
+	ThrottledTimeSeconds float64
+	CpuQuotaCores        float64
+
+	MemoryRssBytes   float64
+	MemoryCacheBytes float64
+	MemoryLimitBytes float64
+
+	Blkio map[string]BlkioStat
+}
+
+type Cgroup struct {
+	Id            string
+	ContainerType ContainerType
+	ContainerId   string
+
+	prevStats *Stats
+
+	subsystems map[string]string
+}
+
+func (cg *Cgroup) GetStats() *Stats {
+	cur, err := cg.getCurrentStats()
+	if err != nil {
+		if !common.IsNotExist(err) {
+			klog.Warningf("failed to get cgroup stats: %s", err)
+		}
+		return nil
+	}
+	var res *Stats
+	if cg.prevStats != nil {
+		res = &Stats{
+			CpuUsageSeconds:      cur.CpuUsageSeconds - cg.prevStats.CpuUsageSeconds,
+			ThrottledTimeSeconds: cur.ThrottledTimeSeconds - cg.prevStats.ThrottledTimeSeconds,
+			CpuQuotaCores:        cur.CpuQuotaCores,
+			MemoryRssBytes:       cur.MemoryRssBytes,
+			MemoryCacheBytes:     cur.MemoryCacheBytes,
+			MemoryLimitBytes:     cur.MemoryLimitBytes,
+			Blkio:                map[string]BlkioStat{},
+		}
+		for majorMinor, stat := range cur.Blkio {
+			prev, ok := cg.prevStats.Blkio[majorMinor]
+			if !ok {
+				continue
+			}
+			res.Blkio[majorMinor] = BlkioStat{
+				ReadOps:      stat.ReadOps - prev.ReadOps,
+				WriteOps:     stat.WriteOps - prev.WriteOps,
+				ReadBytes:    stat.ReadBytes - prev.ReadBytes,
+				WrittenBytes: stat.WrittenBytes - prev.WrittenBytes,
+			}
+		}
+	}
+	cg.prevStats = cur
+	return res
+}
+
+func (cg *Cgroup) getCurrentStats() (*Stats, error) {
+	stats := &Stats{}
+	var err error
+	if stats.CpuUsageSeconds, err = cg.CpuUsageSeconds(); err != nil {
+		return nil, err
+	}
+	if stats.ThrottledTimeSeconds, err = cg.ThrottledTimeSeconds(); err != nil {
+		return nil, err
+	}
+	if stats.CpuQuotaCores, err = cg.CpuQuotaCores(); err != nil {
+		return nil, err
+	}
+	m, err := cg.MemoryStat()
+	if err != nil {
+		return nil, err
+	}
+	stats.MemoryRssBytes = float64(m.RSS)
+	stats.MemoryCacheBytes = float64(m.Cache)
+	l, err := cg.MemoryLimitBytes()
+	if err != nil {
+		return nil, err
+	}
+	stats.MemoryLimitBytes = float64(l)
+	stats.Blkio, err = cg.BlkioStat()
+	if err != nil {
+		return nil, err
+	}
+	return stats, nil
+}
+
+func (cg *Cgroup) CreatedAt() time.Time {
+	fi, err := os.Stat(path.Join(cgRoot, "cpu", cg.subsystems["cpu"]))
+	if err != nil {
+		if !common.IsNotExist(err) {
+			klog.Errorln(err)
+		}
+		return time.Time{}
+	}
+	return fi.ModTime()
+}
+
+func NewFromProcessCgroupFile(filePath string) (*Cgroup, error) {
+	data, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return nil, err
+	}
+	cg := &Cgroup{
+		subsystems: map[string]string{},
+	}
+	for _, line := range strings.Split(string(data), "\n") {
+		parts := strings.SplitN(line, ":", 3)
+		if len(parts) < 3 {
+			continue
+		}
+		for _, cgType := range strings.Split(parts[1], ",") {
+			cg.subsystems[cgType] = parts[2]
+		}
+	}
+	cg.Id = cg.subsystems["cpu"]
+	if cg.ContainerType, cg.ContainerId, err = containerByCgroup(cg.Id); err != nil {
+		return nil, err
+	}
+	return cg, nil
+}
+
+func containerByCgroup(path string) (ContainerType, string, error) {
+	prefix := strings.Split(strings.TrimLeft(path, "/"), "/")[0]
+	switch prefix {
+	case "", "user.slice", "init.scope":
+		return ContainerTypeStandaloneProcess, "", nil
+	case "docker":
+		matches := dockerIdRegexp.FindStringSubmatch(path)
+		if matches == nil {
+			return ContainerTypeUnknown, "", fmt.Errorf("invalid docker cgroup %s", path)
+		}
+		return ContainerTypeDocker, matches[1], nil
+	case "kubepods":
+		crioMatches := crioIdRegexp.FindStringSubmatch(path)
+		if crioMatches != nil {
+			return ContainerTypeCrio, crioMatches[1], nil
+		}
+		matches := dockerIdRegexp.FindStringSubmatch(path)
+		if matches == nil {
+			return ContainerTypeUnknown, "", fmt.Errorf("invalid docker cgroup %s", path)
+		}
+		return ContainerTypeDocker, matches[1], nil
+	case "lxc":
+		matches := lxcIdRegexp.FindStringSubmatch(path)
+		if matches == nil {
+			return ContainerTypeUnknown, "", fmt.Errorf("invalid lxc cgroup %s", path)
+		}
+		return ContainerTypeLxc, matches[1], nil
+	case "system.slice":
+		matches := systemSliceIdRegexp.FindStringSubmatch(path)
+		if matches == nil {
+			return ContainerTypeUnknown, "", fmt.Errorf("invalid systemd cgroup %s", path)
+		}
+		return ContainerTypeSystemdService, matches[1], nil
+	}
+	return ContainerTypeUnknown, "", fmt.Errorf("unknown container: %s", path)
+}

+ 91 - 0
cgroup/cgroup_test.go

@@ -0,0 +1,91 @@
+package cgroup
+
+import (
+	"github.com/stretchr/testify/assert"
+	"path"
+	"testing"
+)
+
+func TestNewFromProcessCgroupFile(t *testing.T) {
+	cg, err := NewFromProcessCgroupFile(path.Join("fixtures/proc/100/cgroup"))
+	assert.Nil(t, err)
+	assert.Equal(t, "/system.slice/ssh.service", cg.Id)
+	assert.Equal(t, "/system.slice/ssh.service", cg.ContainerId)
+	assert.Equal(t, ContainerTypeSystemdService, cg.ContainerType)
+
+	assert.Equal(t,
+		map[string]string{
+			"blkio":        "/system.slice/ssh.service",
+			"cpu":          "/system.slice/ssh.service",
+			"cpuacct":      "/system.slice/ssh.service",
+			"cpuset":       "/",
+			"devices":      "/system.slice/ssh.service",
+			"freezer":      "/",
+			"hugetlb":      "/",
+			"memory":       "/system.slice/ssh.service",
+			"name=systemd": "/system.slice/ssh.service",
+			"net_cls":      "/",
+			"net_prio":     "/",
+			"perf_event":   "/",
+			"pids":         "/system.slice/ssh.service",
+		},
+		cg.subsystems,
+	)
+
+	cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/200/cgroup"))
+	assert.Nil(t, err)
+	assert.Equal(t, "/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f", cg.Id)
+	assert.Equal(t, "b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f", cg.ContainerId)
+	assert.Equal(t, ContainerTypeDocker, cg.ContainerType)
+
+	cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/300/cgroup"))
+	assert.Nil(t, err)
+	assert.Equal(t, "/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a", cg.Id)
+	assert.Equal(t, "17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a", cg.ContainerId)
+	assert.Equal(t, ContainerTypeDocker, cg.ContainerType)
+
+}
+
+func TestContainerByCgroup(t *testing.T) {
+	as := assert.New(t)
+
+	typ, id, err := containerByCgroup("/kubepods/burstable/pod9729a196c4723b60ab401eaff722982d/d166c6190614efc91956b78e96d74c3fbc96ca8e91948c36de3bc5b0e7b27d48")
+	as.Equal(typ, ContainerTypeDocker)
+	as.Equal("d166c6190614efc91956b78e96d74c3fbc96ca8e91948c36de3bc5b0e7b27d48", id)
+	as.Nil(err)
+
+	typ, id, err = containerByCgroup("/kubepods/besteffort/pod0d08203e-255a-11e9-8cd9-0007cb0b2cc8/671a50f5d60566556912f61511d0ec9e4d5c78d53fbc4676727180438bbbbc55/kube-proxy")
+	as.Equal(typ, ContainerTypeDocker)
+	as.Equal("671a50f5d60566556912f61511d0ec9e4d5c78d53fbc4676727180438bbbbc55", id)
+	as.Nil(err)
+
+	typ, id, err = containerByCgroup("/kubepods/poda38c12e8-255a-11e9-8cd9-0007cb0b2cc8/32c562ed81a2622b37b80cb216859820ba51bd694f60ee4cf10d07a4011266f8")
+	as.Equal(typ, ContainerTypeDocker)
+	as.Equal("32c562ed81a2622b37b80cb216859820ba51bd694f60ee4cf10d07a4011266f8", id)
+	as.Nil(err)
+
+	typ, id, err = containerByCgroup("/docker/63425c4a8b4291744a79dd9011fddc7a1f8ffda61f65d72196aa01d00cae2e2d")
+	as.Equal(typ, ContainerTypeDocker)
+	as.Equal("63425c4a8b4291744a79dd9011fddc7a1f8ffda61f65d72196aa01d00cae2e2d", id)
+	as.Nil(err)
+
+	typ, id, err = containerByCgroup("/lxc/mysql-primary-db")
+	as.Equal(typ, ContainerTypeLxc)
+	as.Equal("mysql-primary-db", id)
+	as.Nil(err)
+
+	typ, id, err = containerByCgroup("/kubepods/poda48c12e8-255a-11e9-8cd9-0007cb0b2cc8/crio-63425c4a8b4291744a79dd9011fddc7a1f8ffda61f65d72196aa01d00cae2e2e")
+	as.Equal(typ, ContainerTypeCrio)
+	as.Equal("63425c4a8b4291744a79dd9011fddc7a1f8ffda61f65d72196aa01d00cae2e2e", id)
+	as.Nil(err)
+
+	typ, id, err = containerByCgroup("/system.slice/system-serial\\x2dgetty.slice")
+	as.Equal(typ, ContainerTypeSystemdService)
+	as.Equal("/system.slice/system-serial\\x2dgetty.slice", id)
+	as.Nil(err)
+
+	typ, id, err = containerByCgroup("/system.slice/system-postgresql.slice/[email protected]")
+	as.Equal(typ, ContainerTypeSystemdService)
+	as.Equal("/system.slice/system-postgresql.slice", id)
+	as.Nil(err)
+}

+ 42 - 0
cgroup/cpu.go

@@ -0,0 +1,42 @@
+package cgroup
+
+import (
+	"path"
+)
+
+type ThrottlingStat struct {
+	Periods              uint64
+	ThrottledPeriods     uint64
+	ThrottledTimeSeconds float64
+}
+
+func (cg Cgroup) ThrottledTimeSeconds() (float64, error) {
+	vars, err := readVariablesFromFile(path.Join(cgRoot, "cpu", cg.subsystems["cpu"], "cpu.stat"))
+	if err != nil {
+		return 0, err
+	}
+	return float64(vars["throttled_time"]) / 1e9, nil
+}
+
+func (cg Cgroup) CpuUsageSeconds() (float64, error) {
+	usageNs, err := readIntFromFile(path.Join(cgRoot, "cpuacct", cg.subsystems["cpuacct"], "cpuacct.usage"))
+	if err != nil {
+		return 0, err
+	}
+	return float64(usageNs) / 1e9, err
+}
+
+func (cg Cgroup) CpuQuotaCores() (float64, error) {
+	periodUs, err := readIntFromFile(path.Join(cgRoot, "cpu", cg.subsystems["cpu"], "cpu.cfs_period_us"))
+	if err != nil {
+		return -1, err
+	}
+	quotaUs, err := readIntFromFile(path.Join(cgRoot, "cpu", cg.subsystems["cpu"], "cpu.cfs_quota_us"))
+	if err != nil {
+		return -1, err
+	}
+	if quotaUs < 0 {
+		return -1, nil
+	}
+	return float64(quotaUs) / float64(periodUs), nil
+}

+ 39 - 0
cgroup/cpu_test.go

@@ -0,0 +1,39 @@
+package cgroup
+
+import (
+	"github.com/stretchr/testify/assert"
+	"path"
+	"testing"
+)
+
+func TestCgroup_CpuQuotaCores(t *testing.T) {
+	cgRoot = "fixtures/cgroup"
+
+	cg, _ := NewFromProcessCgroupFile(path.Join("fixtures/proc/100/cgroup"))
+	quota, err := cg.CpuQuotaCores()
+	assert.Nil(t, err)
+	assert.Equal(t, -1., quota)
+
+	cg, _ = NewFromProcessCgroupFile(path.Join("fixtures/proc/200/cgroup"))
+	quota, err = cg.CpuQuotaCores()
+	assert.Nil(t, err)
+	assert.Equal(t, 1.5, quota)
+}
+
+func TestCgroup_CpuUsageSeconds(t *testing.T) {
+	cgRoot = "fixtures/cgroup"
+
+	cg, _ := NewFromProcessCgroupFile(path.Join("fixtures/proc/100/cgroup"))
+	usage, err := cg.CpuUsageSeconds()
+	assert.Nil(t, err)
+	assert.Equal(t, 26778.913419246, usage)
+}
+
+func TestCgroup_ThrottlingStat(t *testing.T) {
+	cgRoot = "fixtures/cgroup"
+
+	cg, _ := NewFromProcessCgroupFile(path.Join("fixtures/proc/200/cgroup"))
+	tt, err := cg.ThrottledTimeSeconds()
+	assert.Nil(t, err)
+	assert.Equal(t, 254005.032764376, tt)
+}

+ 26 - 0
cgroup/fixtures/cgroup/blkio/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/blkio.throttle.io_service_bytes

@@ -0,0 +1,26 @@
+8:16 Read 0
+8:16 Write 655360
+8:16 Sync 49152
+8:16 Async 606208
+8:16 Total 655360
+8:0 Read 0
+8:0 Write 4603904
+8:0 Sync 229376
+8:0 Async 4374528
+8:0 Total 4603904
+8:48 Read 875529547776
+8:48 Write 753046432768
+8:48 Sync 753018997760
+8:48 Async 875556982784
+8:48 Total 1628575980544
+8:32 Read 998632854016
+8:32 Write 884175858688
+8:32 Sync 884138183680
+8:32 Async 998670529024
+8:32 Total 1882808712704
+9:1 Read 10238894080
+9:1 Write 49152
+9:1 Sync 0
+9:1 Async 10238943232
+9:1 Total 10238943232
+Total 3521628895744

+ 26 - 0
cgroup/fixtures/cgroup/blkio/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/blkio.throttle.io_serviced

@@ -0,0 +1,26 @@
+8:16 Read 0
+8:16 Write 39
+8:16 Sync 2
+8:16 Async 37
+8:16 Total 39
+8:0 Read 0
+8:0 Write 281
+8:0 Sync 14
+8:0 Async 267
+8:0 Total 281
+8:48 Read 20689345
+8:48 Write 27906791
+8:48 Sync 27900737
+8:48 Async 20695399
+8:48 Total 48596136
+8:32 Read 23043666
+8:32 Write 28906992
+8:32 Sync 28898397
+8:32 Async 23052261
+8:32 Total 51950658
+9:1 Read 633949
+9:1 Write 4
+9:1 Sync 0
+9:1 Async 633953
+9:1 Total 633953
+Total 101181067

+ 2 - 0
cgroup/fixtures/cgroup/cpu/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/cpu.cfs_period_us

@@ -0,0 +1,2 @@
+100000
+

+ 2 - 0
cgroup/fixtures/cgroup/cpu/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/cpu.cfs_quota_us

@@ -0,0 +1,2 @@
+150000
+

+ 3 - 0
cgroup/fixtures/cgroup/cpu/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/cpu.stat

@@ -0,0 +1,3 @@
+nr_periods 380975143
+nr_throttled 5583849
+throttled_time 254005032764376

+ 1 - 0
cgroup/fixtures/cgroup/cpu/system.slice/ssh.service/cpu.cfs_period_us

@@ -0,0 +1 @@
+100000

+ 1 - 0
cgroup/fixtures/cgroup/cpu/system.slice/ssh.service/cpu.cfs_quota_us

@@ -0,0 +1 @@
+-1

+ 3 - 0
cgroup/fixtures/cgroup/cpu/system.slice/ssh.service/cpu.stat

@@ -0,0 +1,3 @@
+nr_periods 0
+nr_throttled 0
+throttled_time 0

+ 1 - 0
cgroup/fixtures/cgroup/cpuacct/system.slice/ssh.service/cpuacct.usage

@@ -0,0 +1 @@
+26778913419246

+ 1 - 0
cgroup/fixtures/cgroup/memory/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/memory.limit_in_bytes

@@ -0,0 +1 @@
+21474836480

+ 32 - 0
cgroup/fixtures/cgroup/memory/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f/memory.stat

@@ -0,0 +1,32 @@
+cache 3206844416
+rss 12778020864
+rss_huge 11370758144
+mapped_file 1997103104
+dirty 31039488
+writeback 0
+pgpgin 5542721218
+pgpgout 5566438724
+pgfault 4304414697
+pgmajfault 44816508
+inactive_anon 0
+active_anon 12777906176
+inactive_file 1601998848
+active_file 1604784128
+unevictable 0
+hierarchical_memory_limit 21474836480
+total_cache 3206844416
+total_rss 12778020864
+total_rss_huge 11370758144
+total_mapped_file 1997103104
+total_dirty 31039488
+total_writeback 0
+total_pgpgin 5542721218
+total_pgpgout 5566438724
+total_pgfault 4304414697
+total_pgmajfault 44816508
+total_inactive_anon 0
+total_active_anon 12777906176
+total_inactive_file 1601998848
+total_active_file 1604784128
+total_unevictable 0
+

+ 1 - 0
cgroup/fixtures/cgroup/memory/system.slice/ssh.service/memory.limit_in_bytes

@@ -0,0 +1 @@
+9223372036854771712

+ 11 - 0
cgroup/fixtures/proc/100/cgroup

@@ -0,0 +1,11 @@
+11:perf_event:/
+10:devices:/system.slice/ssh.service
+9:freezer:/
+8:cpuset:/
+7:cpu,cpuacct:/system.slice/ssh.service
+6:blkio:/system.slice/ssh.service
+5:memory:/system.slice/ssh.service
+4:hugetlb:/
+3:net_cls,net_prio:/
+2:pids:/system.slice/ssh.service
+1:name=systemd:/system.slice/ssh.service

+ 11 - 0
cgroup/fixtures/proc/200/cgroup

@@ -0,0 +1,11 @@
+11:perf_event:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+10:devices:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+9:freezer:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+8:cpuset:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+7:cpu,cpuacct:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+6:blkio:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+5:memory:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+4:hugetlb:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+3:net_cls,net_prio:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+2:pids:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f
+1:name=systemd:/docker/b43d92bf1e5c6f78bb9b7bc6f40721280299855ba692092716e3a1b6c0b86f3f

+ 11 - 0
cgroup/fixtures/proc/300/cgroup

@@ -0,0 +1,11 @@
+11:devices:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+10:memory:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+9:hugetlb:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+8:cpuset:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+7:pids:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+6:freezer:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+5:perf_event:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+4:blkio:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+3:cpu,cpuacct:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+2:net_cls,net_prio:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a
+1:name=systemd:/kubepods/burstable/pod6a4ce4a0-ba47-11ea-b2a7-0cc47ac5979e/17db96a24ae1e9dd57143e62b1cb0d2d35e693c65c774c7470e87b0572e07c1a

+ 42 - 0
cgroup/memory.go

@@ -0,0 +1,42 @@
+package cgroup
+
+import (
+	"path"
+)
+
+const maxMemory = 1 << 62
+
+type MemoryStat struct {
+	RSS   uint64
+	Cache uint64
+}
+
+func (cg *Cgroup) MemoryStat() (MemoryStat, error) {
+	vars, err := readVariablesFromFile(path.Join(cgRoot, "memory", cg.subsystems["memory"], "memory.stat"))
+	if err != nil {
+		return MemoryStat{}, err
+	}
+	// Note from https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt:
+	// Only anonymous and swap cache memory is listed as part of 'rss' stat.
+	//	This should not be confused with the true 'resident set size' or the
+	//	amount of physical memory used by the cgroup.
+	//	'rss + mapped_file" will give you resident set size of cgroup.
+	//	(Note: file and shmem may be shared among other cgroups. In that case,
+	//	 mapped_file is accounted only when the memory cgroup is owner of page
+	//	 cache.)
+	return MemoryStat{
+		RSS:   vars["rss"] + vars["mapped_file"],
+		Cache: vars["cache"],
+	}, nil
+}
+
+func (cg *Cgroup) MemoryLimitBytes() (uint64, error) {
+	limit, err := readUintFromFile(path.Join(cgRoot, "memory", cg.subsystems["memory"], "memory.limit_in_bytes"))
+	if err != nil {
+		return 0, err
+	}
+	if limit > maxMemory {
+		return 0, nil
+	}
+	return limit, nil
+}

+ 32 - 0
cgroup/memory_test.go

@@ -0,0 +1,32 @@
+package cgroup
+
+import (
+	"github.com/stretchr/testify/assert"
+	"path"
+	"testing"
+)
+
+func TestCgroup_MemoryStat(t *testing.T) {
+	cgRoot = "fixtures/cgroup"
+
+	cg, _ := NewFromProcessCgroupFile(path.Join("fixtures/proc/200/cgroup"))
+	stat, err := cg.MemoryStat()
+	assert.Nil(t, err)
+	assert.Equal(t, uint64(14775123968), stat.RSS)
+	assert.Equal(t, uint64(3206844416), stat.Cache)
+}
+
+func TestCgroup_MemoryLimitBytes(t *testing.T) {
+	cgRoot = "fixtures/cgroup"
+
+	cg, _ := NewFromProcessCgroupFile(path.Join("fixtures/proc/100/cgroup"))
+	limit, err := cg.MemoryLimitBytes()
+	assert.Nil(t, err)
+	assert.Equal(t, uint64(0), limit)
+
+	cg, _ = NewFromProcessCgroupFile(path.Join("fixtures/proc/200/cgroup"))
+	limit, err = cg.MemoryLimitBytes()
+	assert.Nil(t, err)
+	assert.Equal(t, uint64(21474836480), limit)
+
+}

+ 44 - 0
cgroup/utils.go

@@ -0,0 +1,44 @@
+package cgroup
+
+import (
+	"io/ioutil"
+	"k8s.io/klog/v2"
+	"strconv"
+	"strings"
+)
+
+func readVariablesFromFile(filePath string) (map[string]uint64, error) {
+	data, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return nil, err
+	}
+	res := map[string]uint64{}
+	for _, line := range strings.Split(string(data), "\n") {
+		parts := strings.Fields(line)
+		if len(parts) == 2 {
+			v, err := strconv.ParseUint(parts[1], 10, 64)
+			if err != nil {
+				klog.Warningf(`failed to parse cgroup stat line "%s": %s`, line, err)
+				continue
+			}
+			res[parts[0]] = v
+		}
+	}
+	return res, nil
+}
+
+func readIntFromFile(filePath string) (int64, error) {
+	data, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return 0, err
+	}
+	return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
+}
+
+func readUintFromFile(filePath string) (uint64, error) {
+	data, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return 0, err
+	}
+	return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
+}

+ 7 - 0
common/error.go

@@ -0,0 +1,7 @@
+package common
+
+import "strings"
+
+func IsNotExist(err error) bool {
+	return strings.Contains(err.Error(), "no such file or directory") && strings.Contains(err.Error(), "no such process")
+}

+ 11 - 0
common/kernel.go

@@ -0,0 +1,11 @@
+package common
+
+import "regexp"
+
+var (
+	kernelVersionRe = regexp.MustCompile(`^(\d+\.\d+)`)
+)
+
+func KernelMajorMinor(version string) string {
+	return kernelVersionRe.FindString(version)
+}

+ 16 - 0
common/net.go

@@ -0,0 +1,16 @@
+package common
+
+import (
+	"inet.af/netaddr"
+)
+
+func IsIpPrivate(ip netaddr.IP) bool {
+	if ip.IsPrivate() {
+		return true
+	}
+	if ip.Is4() {
+		parts := ip.As4()
+		return parts[0] == 100 && parts[1]&0xc0 == 64 // 100.64.0.0/10
+	}
+	return false
+}

+ 80 - 0
containers/app.go

@@ -0,0 +1,80 @@
+package containers
+
+import (
+	"bytes"
+)
+
+func guessApplicationType(cmdline []byte) string {
+	parts := bytes.Split([]byte(cmdline), []byte{0})
+	cmd := bytes.TrimSuffix(bytes.Fields(parts[0])[0], []byte{':'})
+	switch {
+	case bytes.HasSuffix(cmd, []byte("memcached")):
+		return "memcached"
+	case bytes.HasSuffix(cmd, []byte("envoy")):
+		return "envoy"
+	case bytes.Contains(cmdline, []byte("org.elasticsearch.bootstrap")):
+		return "elasticsearch"
+	case bytes.Contains(cmdline, []byte("kafka.Kafka")) || bytes.Contains(cmdline, []byte("io.confluent.support.metrics.SupportedKafka")):
+		return "kafka"
+	case bytes.HasSuffix(cmd, []byte("mongod")):
+		return "mongodb"
+	case bytes.HasSuffix(cmd, []byte("mysqld")):
+		return "mysql"
+	case bytes.Contains(cmdline, []byte("org.apache.zookeeper.server.quorum.QuorumPeerMain")):
+		return "zookeeper"
+	case bytes.HasSuffix(cmd, []byte("redis-server")):
+		return "redis"
+	case bytes.HasSuffix(cmd, []byte("redis-sentinel")):
+		return "redis-sentinel"
+	case bytes.HasSuffix(cmd, []byte("beam.smp")) && bytes.Contains(cmdline, []byte("rabbit")):
+		return "rabbitmq"
+	case bytes.HasSuffix(cmd, []byte("beam.smp")) && bytes.Contains(cmdline, []byte("couch")):
+		return "couchbase"
+	case bytes.HasSuffix(cmd, []byte("pgbouncer")):
+		return "pgbouncer"
+	case bytes.HasSuffix(cmd, []byte("postgres")):
+		return "postgres"
+	case bytes.HasSuffix(cmd, []byte("haproxy")):
+		return "haproxy"
+	case bytes.HasSuffix(cmd, []byte("nginx")):
+		return "nginx"
+	case bytes.HasSuffix(cmd, []byte("kubelet")):
+		return "kubelet"
+	case bytes.HasSuffix(cmd, []byte("kube-apiserver")):
+		return "kube-apiserver"
+	case bytes.HasSuffix(cmd, []byte("kube-controller-manager")):
+		return "kube-controller-manager"
+	case bytes.HasSuffix(cmd, []byte("kube-scheduler")):
+		return "kube-scheduler"
+	case bytes.HasSuffix(cmd, []byte("etcd")):
+		return "etcd"
+	case bytes.HasSuffix(cmd, []byte("dockerd")):
+		return "dockerd"
+	case bytes.HasSuffix(cmd, []byte("consul")):
+		return "consul"
+	case bytes.Contains(cmdline, []byte("org.apache.cassandra.service.CassandraDaemon")):
+		return "cassandra"
+	case bytes.HasSuffix(cmd, []byte("clickhouse-server")):
+		return "clickhouse"
+	case bytes.HasSuffix(cmd, []byte("traefik")):
+		return "traefik"
+	case bytes.HasSuffix(cmd, []byte("asd")):
+		return "aerospike"
+	case bytes.HasSuffix(cmd, []byte("httpd")):
+		return "httpd"
+	case bytes.HasSuffix(cmd, []byte("influxd")):
+		return "influxdb"
+	case bytes.Contains(cmdline, []byte("org.apache.catalina.startup.Bootstrap")):
+		return "tomcat"
+	case bytes.HasSuffix(cmd, []byte("vault")):
+		return "vault"
+	case bytes.HasSuffix(cmd, []byte("proxysql")):
+		return "proxysql"
+	case bytes.HasSuffix(cmd, []byte("cockroach")):
+		return "cockroach"
+	case bytes.HasSuffix(cmd, []byte("prometheus")):
+		return "prometheus"
+	}
+	//todo: ceph services, php-fpm, python, nodejs, java
+	return ""
+}

+ 60 - 0
containers/conntrack.go

@@ -0,0 +1,60 @@
+package containers
+
+import (
+	"github.com/florianl/go-conntrack"
+	"inet.af/netaddr"
+	"syscall"
+)
+
+var (
+	conntrackClient *conntrack.Nfct
+)
+
+func ConntrackInit() error {
+	c, err := conntrack.Open(&conntrack.Config{})
+	if err != nil {
+		return err
+	}
+	conntrackClient = c
+	return nil
+}
+
+func ConntrackGetActualDestination(src, dst netaddr.IPPort) (netaddr.IPPort, error) {
+	if conntrackClient == nil {
+		return dst, nil
+	}
+
+	tcp := uint8(syscall.IPPROTO_TCP)
+	sip := src.IP().IPAddr().IP
+	dip := dst.IP().IPAddr().IP
+	sport := src.Port()
+	dport := dst.Port()
+
+	con := conntrack.Con{
+		Origin: &conntrack.IPTuple{
+			Src: &sip,
+			Dst: &dip,
+			Proto: &conntrack.ProtoTuple{
+				Number:  &tcp,
+				SrcPort: &sport,
+				DstPort: &dport,
+			},
+		},
+	}
+	family := conntrack.IPv4
+	if dst.IP().Is6() {
+		family = conntrack.IPv6
+	}
+	sessions, err := conntrackClient.Get(conntrack.Conntrack, family, con)
+	if err != nil {
+		return netaddr.IPPort{}, err
+	}
+	for _, s := range sessions {
+		if s.Reply != nil && s.Reply.Src != nil && s.Reply.Proto != nil && s.Reply.Proto.SrcPort != nil {
+			ip, _ := netaddr.FromStdIP(*s.Reply.Src)
+			port := *s.Reply.Proto.SrcPort
+			return netaddr.IPPortFrom(ip, port), nil
+		}
+	}
+	return netaddr.IPPort{}, nil
+}

+ 742 - 0
containers/container.go

@@ -0,0 +1,742 @@
+package containers
+
+import (
+	"github.com/coroot/coroot-node-agent/cgroup"
+	"github.com/coroot/coroot-node-agent/common"
+	"github.com/coroot/coroot-node-agent/flags"
+	"github.com/coroot/coroot-node-agent/logs"
+	"github.com/coroot/coroot-node-agent/node"
+	"github.com/coroot/coroot-node-agent/pinger"
+	"github.com/coroot/coroot-node-agent/proc"
+	"github.com/coroot/logparser"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/vishvananda/netns"
+	"inet.af/netaddr"
+	"k8s.io/klog/v2"
+	"os"
+	"strings"
+	"sync"
+	"time"
+)
+
+var (
+	gcInterval  = 10 * time.Minute
+	pingTimeout = 300 * time.Millisecond
+)
+
+type ContainerID string
+
+type ContainerMetadata struct {
+	name        string
+	labels      map[string]string
+	volumes     map[string]Volume
+	logPath     string
+	logDecoder  logparser.Decoder
+	hostListens map[string][]netaddr.IPPort
+}
+
+type Volume struct {
+	provisioner string
+	volume      string
+}
+
+type Delays struct {
+	cpu  time.Duration
+	disk time.Duration
+}
+
+type LogParser struct {
+	parser *logparser.Parser
+	stop   func()
+}
+
+func (p *LogParser) Stop() {
+	if p.stop != nil {
+		p.stop()
+	}
+	p.parser.Stop()
+}
+
+type AddrPair struct {
+	src netaddr.IPPort
+	dst netaddr.IPPort
+}
+
+type Container struct {
+	cgroup   *cgroup.Cgroup
+	metadata *ContainerMetadata
+
+	pids map[uint32]time.Time // pid -> start time
+
+	startedAt time.Time
+	zombieAt  time.Time
+	restarts  int
+
+	delays      Delays
+	delaysByPid map[uint32]Delays
+	delaysLock  sync.Mutex
+
+	listens map[netaddr.IPPort]map[uint32]time.Time // listen addr -> pid -> close time
+
+	connectsSuccessful map[AddrPair]int             // dst:actual_dst -> count
+	connectsFailed     map[netaddr.IPPort]int       // dst -> count
+	connectLastAttempt map[netaddr.IPPort]time.Time // dst -> time
+	connectionsActive  map[AddrPair]netaddr.IPPort  // src:dst -> actual_dst
+	retransmits        map[AddrPair]int             // dst:actual_dst -> count
+
+	oomKills int
+
+	mountIds map[string]struct{}
+
+	logParsers map[string]*LogParser
+
+	lock sync.RWMutex
+
+	done chan struct{}
+}
+
+func NewContainer(cg *cgroup.Cgroup, md *ContainerMetadata) *Container {
+	c := &Container{
+		cgroup:   cg,
+		metadata: md,
+
+		pids: map[uint32]time.Time{},
+
+		delaysByPid: map[uint32]Delays{},
+
+		listens: map[netaddr.IPPort]map[uint32]time.Time{},
+
+		connectsSuccessful: map[AddrPair]int{},
+		connectsFailed:     map[netaddr.IPPort]int{},
+		connectLastAttempt: map[netaddr.IPPort]time.Time{},
+		connectionsActive:  map[AddrPair]netaddr.IPPort{},
+		retransmits:        map[AddrPair]int{},
+
+		mountIds: map[string]struct{}{},
+
+		logParsers: map[string]*LogParser{},
+
+		done: make(chan struct{}),
+	}
+
+	c.runLogParser("")
+
+	go func() {
+		ticker := time.NewTicker(gcInterval)
+		defer ticker.Stop()
+		for {
+			select {
+			case <-c.done:
+				return
+			case t := <-ticker.C:
+				c.gc(t)
+			}
+		}
+	}()
+
+	return c
+}
+
+func (c *Container) Close() {
+	for _, p := range c.logParsers {
+		p.Stop()
+	}
+	close(c.done)
+}
+
+func (c *Container) Dead(now time.Time) bool {
+	return !c.zombieAt.IsZero() && now.Sub(c.zombieAt) > gcInterval
+}
+
+func (c *Container) Describe(ch chan<- *prometheus.Desc) {
+	for _, m := range metricsList {
+		ch <- m
+	}
+}
+
+func (c *Container) Collect(ch chan<- prometheus.Metric) {
+	c.lock.RLock()
+	defer c.lock.RUnlock()
+
+	ch <- counter(metrics.Restarts, float64(c.restarts))
+
+	if v, err := c.cgroup.CpuQuotaCores(); err == nil && v > 0 {
+		ch <- gauge(metrics.CPULimit, v)
+	}
+	if v, err := c.cgroup.CpuUsageSeconds(); err == nil {
+		ch <- counter(metrics.CPUUsage, v)
+	}
+	if v, err := c.cgroup.ThrottledTimeSeconds(); err == nil {
+		ch <- counter(metrics.ThrottledTime, v)
+	}
+
+	if taskstatsClient != nil {
+		c.updateDelays()
+		ch <- counter(metrics.CPUDelay, float64(c.delays.cpu)/float64(time.Second))
+		ch <- counter(metrics.DiskDelay, float64(c.delays.disk)/float64(time.Second))
+	}
+
+	if v, err := c.cgroup.MemoryLimitBytes(); err == nil && v > 0 {
+		ch <- gauge(metrics.MemoryLimit, float64(v))
+	}
+	if s, err := c.cgroup.MemoryStat(); err == nil {
+		ch <- gauge(metrics.MemoryRss, float64(s.RSS))
+		ch <- gauge(metrics.MemoryCache, float64(s.Cache))
+	}
+
+	if c.oomKills > 0 {
+		ch <- counter(metrics.OOMKills, float64(c.oomKills))
+	}
+
+	if disks, err := node.GetDisks(); err == nil {
+		ioStat, _ := c.cgroup.BlkioStat()
+		for majorMinor, mounts := range c.getMounts() {
+			dev := disks.GetParentBlockDevice(majorMinor)
+			if dev == nil {
+				continue
+			}
+			for mountPoint, fsStat := range mounts {
+				v := c.metadata.volumes[mountPoint]
+				dls := []string{mountPoint, dev.Name, v.provisioner, v.volume}
+				ch <- gauge(metrics.DiskSize, float64(fsStat.CapacityBytes), dls...)
+				ch <- gauge(metrics.DiskUsed, float64(fsStat.UsedBytes), dls...)
+				ch <- gauge(metrics.DiskReserved, float64(fsStat.ReservedBytes), dls...)
+				if io, ok := ioStat[majorMinor]; ok {
+					ch <- counter(metrics.DiskReadOps, float64(io.ReadOps), dls...)
+					ch <- counter(metrics.DiskReadBytes, float64(io.ReadBytes), dls...)
+					ch <- counter(metrics.DiskWriteOps, float64(io.WriteOps), dls...)
+					ch <- counter(metrics.DiskWriteBytes, float64(io.WrittenBytes), dls...)
+				}
+			}
+		}
+	}
+
+	netNs := netns.None()
+	for pid := range c.pids {
+		if pid == agentPid {
+			netNs = selfNetNs
+			break
+		}
+		ns, err := proc.GetNetNs(pid)
+		if err != nil {
+			if !common.IsNotExist(err) {
+				klog.Warningln(err)
+			}
+			continue
+		}
+		netNs = ns
+		defer netNs.Close()
+		break
+	}
+
+	listens := c.getListens(netNs)
+	klog.Infof("got listens for %s ns=%s: %v", c.cgroup.Id, netNs.UniqueId(), listens)
+	for addr, open := range listens {
+		ch <- gauge(metrics.NetListenInfo, float64(open), addr.String(), "")
+	}
+	for proxy, addrs := range c.getProxiedListens() {
+		for addr := range addrs {
+			ch <- gauge(metrics.NetListenInfo, 1, addr.String(), proxy)
+		}
+	}
+
+	for d, count := range c.connectsSuccessful {
+		ch <- counter(metrics.NetConnectsSuccessful, float64(count), d.src.String(), d.dst.String())
+	}
+	for dst, count := range c.connectsFailed {
+		ch <- counter(metrics.NetConnectsFailed, float64(count), dst.String())
+	}
+	for d, count := range c.retransmits {
+		ch <- counter(metrics.NetRetransmits, float64(count), d.src.String(), d.dst.String())
+	}
+
+	connections := map[AddrPair]int{}
+	for c, actualDst := range c.connectionsActive {
+		connections[AddrPair{src: c.dst, dst: actualDst}]++
+	}
+	for d, count := range connections {
+		ch <- gauge(metrics.NetConnectionsActive, float64(count), d.src.String(), d.dst.String())
+	}
+
+	for source, p := range c.logParsers {
+		for _, c := range p.parser.GetCounters() {
+			ch <- counter(metrics.LogMessages, float64(c.Messages), source, string(c.Level), c.Hash, c.Sample)
+		}
+	}
+
+	appTypes := map[string]struct{}{}
+	for pid := range c.pids {
+		cmdline := proc.GetCmdline(pid)
+		if len(cmdline) == 0 {
+			continue
+		}
+		appType := guessApplicationType(cmdline)
+		if appType == "" {
+			continue
+		}
+		appTypes[appType] = struct{}{}
+	}
+	for appType := range appTypes {
+		ch <- gauge(metrics.ApplicationType, 1, appType)
+	}
+
+	if !*flags.NoPingUpstreams {
+		for ip, rtt := range c.ping(netNs) {
+			ch <- gauge(metrics.NetLatency, rtt, ip.String())
+		}
+	}
+}
+
+func (c *Container) onProcessStart(pid uint32) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	stats, err := TaskstatsPID(pid)
+	if err != nil {
+		return
+	}
+	c.zombieAt = time.Time{}
+	c.pids[pid] = stats.BeginTime
+	if c.startedAt.IsZero() {
+		c.startedAt = stats.BeginTime
+	} else {
+		min := stats.BeginTime
+		for _, t := range c.pids {
+			if t.Before(min) {
+				min = t
+			}
+		}
+		if min.After(c.startedAt) {
+			c.restarts++
+			c.startedAt = min
+		}
+	}
+}
+
+func (c *Container) onProcessExit(pid uint32, oomKill bool) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	delete(c.pids, pid)
+	if len(c.pids) == 0 {
+		c.zombieAt = time.Now()
+	}
+	delete(c.delaysByPid, pid)
+	if oomKill {
+		c.oomKills++
+	}
+}
+
+func (c *Container) onFileOpen(pid uint32, fd uint32) {
+	mntId, logPath := resolveFd(pid, fd)
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	if mntId != "" {
+		c.mountIds[mntId] = struct{}{}
+	}
+	if logPath != "" {
+		c.runLogParser(logPath)
+	}
+}
+
+func (c *Container) onListenOpen(pid uint32, addr netaddr.IPPort) {
+	netNs, err := proc.GetNetNs(pid)
+	isHostNs := err == nil && hostNetNsId == netNs.UniqueId()
+	_ = netNs.Close()
+	if addr.IP().IsLoopback() && !isHostNs {
+		return
+	}
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	if _, ok := c.listens[addr]; !ok {
+		c.listens[addr] = map[uint32]time.Time{}
+	}
+	c.listens[addr][pid] = time.Time{}
+}
+
+func (c *Container) onListenClose(pid uint32, addr netaddr.IPPort) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	if _, byAddr := c.listens[addr]; byAddr {
+		if _, byPid := c.listens[addr][pid]; byPid {
+			c.listens[addr][pid] = time.Now()
+		}
+	}
+}
+
+func (c *Container) onConnectionOpen(pid uint32, src, dst netaddr.IPPort, failed bool) {
+	if dst.IP().IsLoopback() {
+		netNs, err := proc.GetNetNs(pid)
+		isHostNs := err == nil && hostNetNsId == netNs.UniqueId()
+		netNs.Close()
+		if !isHostNs {
+			return
+		}
+	} else {
+		whitelisted := false
+		for _, prefix := range flags.ExternalNetworksWhitelist {
+			if prefix.Contains(dst.IP()) {
+				whitelisted = true
+				break
+			}
+		}
+		if !whitelisted && !common.IsIpPrivate(dst.IP()) {
+			return
+		}
+	}
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	if failed {
+		c.connectsFailed[dst]++
+	} else {
+		actualDst, err := ConntrackGetActualDestination(src, dst)
+		if err != nil {
+			klog.Errorf("failed to resolve actual destination for %s->%s: %s", src, dst, err)
+		} else if actualDst.IsValid() {
+			c.connectsSuccessful[AddrPair{src: dst, dst: actualDst}]++
+			c.connectionsActive[AddrPair{src: src, dst: dst}] = actualDst
+		} else {
+			klog.Errorf("invalid actual destination for %s->%s: %s", src, dst, actualDst)
+		}
+	}
+	c.connectLastAttempt[dst] = time.Now()
+}
+
+func (c *Container) onConnectionClose(srcDst AddrPair) bool {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	if _, ok := c.connectionsActive[srcDst]; !ok {
+		return false
+	}
+	delete(c.connectionsActive, srcDst)
+	return true
+}
+
+func (c *Container) onRetransmit(srcDst AddrPair) bool {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	actualDst, ok := c.connectionsActive[srcDst]
+	if !ok {
+		return false
+	}
+	c.retransmits[AddrPair{src: srcDst.dst, dst: actualDst}]++
+	return true
+}
+
+func (c *Container) updateDelays() {
+	c.delaysLock.Lock()
+	defer c.delaysLock.Unlock()
+	for pid := range c.pids {
+		stats, err := TaskstatsTGID(pid)
+		if err != nil {
+			continue
+		}
+		d := c.delaysByPid[pid]
+		c.delays.cpu += stats.CPUDelay - d.cpu
+		c.delays.disk += stats.BlockIODelay - d.disk
+		d.cpu = stats.CPUDelay
+		d.disk = stats.BlockIODelay
+		c.delaysByPid[pid] = d
+	}
+}
+
+func (c *Container) getMounts() map[string]map[string]*proc.FSStat {
+	mounts := map[string]proc.MountInfo{}
+	for p := range c.pids {
+		mi := proc.GetMountInfo(p)
+		if mi != nil {
+			mounts = mi
+			break
+		}
+	}
+	for mountId := range mounts {
+		if _, ok := c.mountIds[mountId]; !ok {
+			delete(mounts, mountId)
+		}
+	}
+	if len(mounts) == 0 {
+		return nil
+	}
+	res := map[string]map[string]*proc.FSStat{}
+	for _, mi := range mounts {
+		var stat *proc.FSStat
+		for pid := range c.pids {
+			s, err := proc.StatFS(proc.Path(pid, "root", mi.MountPoint))
+			if err == nil {
+				stat = &s
+				break
+			}
+		}
+		if stat == nil {
+			continue
+		}
+		if _, ok := res[mi.MajorMinor]; !ok {
+			res[mi.MajorMinor] = map[string]*proc.FSStat{}
+		}
+		res[mi.MajorMinor][mi.MountPoint] = stat
+	}
+	return res
+}
+
+func (c *Container) getListens(netNs netns.NsHandle) map[netaddr.IPPort]int {
+	if !netNs.IsOpen() {
+		return nil
+	}
+	isHostNs := hostNetNsId == netNs.UniqueId()
+	res := map[netaddr.IPPort]int{}
+	for addr, byPid := range c.listens {
+		open := 0
+		for _, closedAt := range byPid {
+			if closedAt.IsZero() {
+				open = 1
+				break
+			}
+		}
+		var ips []netaddr.IP
+		if addr.IP().IsUnspecified() {
+			if nsIps, err := proc.GetNsIps(netNs); err != nil {
+				klog.Warningln(err)
+			} else {
+				ips = nsIps
+			}
+		} else {
+			ips = []netaddr.IP{addr.IP()}
+		}
+		for _, ip := range ips {
+			if ip.IsLoopback() && !isHostNs {
+				continue
+			}
+			res[netaddr.IPPortFrom(ip, addr.Port())] = open
+		}
+	}
+	return res
+}
+
+func (c *Container) getProxiedListens() map[string]map[netaddr.IPPort]struct{} {
+	if len(c.metadata.hostListens) == 0 {
+		return nil
+	}
+
+	hasUnspecified := false
+	for _, addrs := range c.metadata.hostListens {
+		for _, addr := range addrs {
+			if addr.IP().IsUnspecified() {
+				hasUnspecified = true
+				break
+			}
+		}
+	}
+
+	var hostIps []netaddr.IP
+	if hasUnspecified {
+		if ns, err := proc.GetHostNetNs(); err != nil {
+			klog.Warningln(err)
+		} else {
+			ips, err := proc.GetNsIps(ns)
+			_ = ns.Close()
+			if err != nil {
+				klog.Warningln(err)
+			} else {
+				hostIps = ips
+			}
+		}
+	}
+
+	res := map[string]map[netaddr.IPPort]struct{}{}
+	for proxy, addrs := range c.metadata.hostListens {
+		res[proxy] = map[netaddr.IPPort]struct{}{}
+		for _, addr := range addrs {
+			if addr.IP().IsUnspecified() {
+				for _, ip := range hostIps {
+					if addr.IP().Is4() && ip.Is4() || addr.IP().Is6() && ip.Is6() {
+						res[proxy][netaddr.IPPortFrom(ip, addr.Port())] = struct{}{}
+					}
+				}
+			} else {
+				res[proxy][addr] = struct{}{}
+			}
+		}
+	}
+	return res
+}
+
+func (c *Container) ping(netNs netns.NsHandle) map[netaddr.IP]float64 {
+	if !netNs.IsOpen() {
+		return nil
+	}
+	ips := map[netaddr.IP]struct{}{}
+	for d := range c.connectsSuccessful {
+		ips[d.dst.IP()] = struct{}{}
+	}
+	for dst := range c.connectsFailed {
+		ips[dst.IP()] = struct{}{}
+	}
+	if len(ips) == 0 {
+		return nil
+	}
+	targets := make([]netaddr.IP, 0, len(ips))
+	for ip := range ips {
+		targets = append(targets, ip)
+	}
+	rtt, err := pinger.Ping(netNs, selfNetNs, targets, pingTimeout)
+	if err != nil {
+		klog.Warningln(err)
+		return nil
+	}
+	return rtt
+}
+
+func (c *Container) runLogParser(logPath string) {
+	if *flags.NoParseLogs {
+		return
+	}
+	switch {
+	case logPath != "":
+		if c.logParsers[logPath] != nil {
+			return
+		}
+		ch := make(chan logparser.LogEntry)
+		parser := logparser.NewParser(ch, nil)
+		reader, err := logs.NewTailReader(proc.HostPath(logPath), ch)
+		if err != nil {
+			klog.Warningln(err)
+			parser.Stop()
+			return
+		}
+		klog.InfoS("started varlog logparser", "cg", c.cgroup.Id, "log", logPath)
+		c.logParsers[logPath] = &LogParser{parser: parser, stop: reader.Stop}
+
+	case c.cgroup.ContainerType == cgroup.ContainerTypeSystemdService:
+		ch := make(chan logparser.LogEntry)
+		if err := JournaldSubscribe(c.cgroup, ch); err != nil {
+			klog.Warningln(err)
+			return
+		}
+		parser := logparser.NewParser(ch, nil)
+		stop := func() {
+			JournaldUnsubscribe(c.cgroup)
+		}
+		klog.InfoS("started journald logparser", "cg", c.cgroup.Id)
+		c.logParsers["journald"] = &LogParser{parser: parser, stop: stop}
+
+	case c.cgroup.ContainerType == cgroup.ContainerTypeDocker:
+		if c.metadata.logPath == "" {
+			return
+		}
+		if parser := c.logParsers["stdout/stderr"]; parser != nil {
+			parser.Stop()
+			delete(c.logParsers, "stdout/stderr")
+		}
+		ch := make(chan logparser.LogEntry)
+		parser := logparser.NewParser(ch, c.metadata.logDecoder)
+		reader, err := logs.NewTailReader(proc.HostPath(c.metadata.logPath), ch)
+		if err != nil {
+			klog.Warningln(err)
+			parser.Stop()
+			return
+		}
+		klog.InfoS("started container logparser", "cg", c.cgroup.Id)
+		c.logParsers["stdout/stderr"] = &LogParser{parser: parser, stop: reader.Stop}
+	}
+}
+
+func (c *Container) gc(now time.Time) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	established := map[AddrPair]struct{}{}
+	establishedDst := map[netaddr.IPPort]struct{}{}
+	listens := map[netaddr.IPPort]struct{}{}
+	for pid := range c.pids {
+		sockets, err := proc.GetSockets(pid)
+		if err != nil {
+			continue
+		}
+		for _, s := range sockets {
+			if s.Listen {
+				listens[s.SAddr] = struct{}{}
+			} else {
+				established[AddrPair{src: s.SAddr, dst: s.DAddr}] = struct{}{}
+				establishedDst[s.DAddr] = struct{}{}
+			}
+		}
+		break
+	}
+
+	for addr, byPid := range c.listens {
+		_, open := listens[addr]
+		if open {
+			continue
+		}
+		for pid, closedAt := range byPid {
+			if closedAt.IsZero() {
+				byPid[pid] = now
+			}
+		}
+	}
+	for pid, addrs := range c.listens {
+		for addr, closedAt := range addrs {
+			if !closedAt.IsZero() && now.Sub(closedAt) > gcInterval {
+				delete(c.listens[pid], addr)
+			}
+		}
+		if len(c.listens[pid]) == 0 {
+			delete(c.listens, pid)
+		}
+	}
+
+	for srcDst := range c.connectionsActive {
+		if _, ok := established[srcDst]; !ok {
+			delete(c.connectionsActive, srcDst)
+		}
+	}
+	for dst, at := range c.connectLastAttempt {
+		_, active := establishedDst[dst]
+		if !active && !at.IsZero() && now.Sub(at) > gcInterval {
+			delete(c.connectLastAttempt, dst)
+			delete(c.connectsFailed, dst)
+			for d := range c.connectsSuccessful {
+				if d.src == dst {
+					delete(c.connectsSuccessful, d)
+				}
+			}
+			for d := range c.retransmits {
+				if d.src == dst {
+					delete(c.retransmits, d)
+				}
+			}
+		}
+	}
+}
+
+func resolveFd(pid uint32, fd uint32) (mntId string, logPath string) {
+	info := proc.GetFdInfo(pid, fd)
+	if info == nil {
+		return
+	}
+	switch {
+	case info.Flags&os.O_WRONLY == 0 && info.Flags&os.O_RDWR == 0,
+		!strings.HasPrefix(info.Dest, "/"),
+		strings.HasPrefix(info.Dest, "/proc/"),
+		strings.HasPrefix(info.Dest, "/dev/"),
+		strings.HasPrefix(info.Dest, "/sys/"),
+		strings.HasSuffix(info.Dest, "(deleted)"):
+		return
+	}
+	mntId = info.MntId
+
+	if info.Flags&os.O_WRONLY != 0 && strings.HasPrefix(info.Dest, "/var/log/") &&
+		!strings.HasPrefix(info.Dest, "/var/log/pods/") &&
+		!strings.HasPrefix(info.Dest, "/var/log/containers/") &&
+		!strings.HasPrefix(info.Dest, "/var/log/journal/") {
+
+		logPath = info.Dest
+	}
+	return
+}
+
+func counter(desc *prometheus.Desc, value float64, labelValues ...string) prometheus.Metric {
+	return prometheus.MustNewConstMetric(desc, prometheus.CounterValue, value, labelValues...)
+}
+
+func gauge(desc *prometheus.Desc, value float64, labelValues ...string) prometheus.Metric {
+	return prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, value, labelValues...)
+}

+ 77 - 0
containers/containerd.go

@@ -0,0 +1,77 @@
+package containers
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/containerd/containerd"
+	"github.com/containerd/containerd/oci"
+	"github.com/containerd/containerd/pkg/cri/constants"
+	"github.com/coroot/coroot-node-agent/proc"
+	"github.com/coroot/logparser"
+	"k8s.io/klog/v2"
+	"time"
+)
+
+const containerdTimeout = 30 * time.Second
+
+var (
+	containerdClient *containerd.Client
+)
+
+func ContainerdInit() error {
+	c, err := containerd.New(proc.HostPath("/run/containerd/containerd.sock"),
+		containerd.WithDefaultNamespace(constants.K8sContainerdNamespace),
+		containerd.WithTimeout(time.Second))
+	if err != nil {
+		return err
+	}
+	containerdClient = c
+	return nil
+}
+
+func ContainerdInspect(containerID string) (*ContainerMetadata, error) {
+	if containerdClient == nil {
+		return nil, fmt.Errorf("containerd client not initialized")
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), containerdTimeout)
+	defer cancel()
+
+	c, err := containerdClient.ContainerService().Get(ctx, containerID)
+	if err != nil {
+		return nil, err
+	}
+
+	res := &ContainerMetadata{
+		labels:  c.Labels,
+		volumes: map[string]Volume{},
+	}
+
+	var spec oci.Spec
+	if err := json.Unmarshal(c.Spec.Value, &spec); err != nil {
+		klog.Warningln(err)
+	} else {
+		for _, m := range spec.Mounts {
+			if provisioner, volume := parseVolumeSource(m.Source); provisioner != "" && volume != "" {
+				res.volumes[m.Destination] = Volume{provisioner: provisioner, volume: volume}
+			}
+		}
+	}
+
+	if data, ok := c.Extensions["io.cri-containerd.container.metadata"]; ok {
+		var md = struct { // from data.TypeUrl
+			Metadata struct {
+				LogPath string
+			}
+		}{}
+		if err := json.Unmarshal(data.Value, &md); err != nil {
+			klog.Warningln(err)
+		} else {
+			res.logPath = md.Metadata.LogPath
+			res.logDecoder = logparser.CriDecoder{}
+		}
+	}
+
+	return res, nil
+}

+ 81 - 0
containers/dockerd.go

@@ -0,0 +1,81 @@
+package containers
+
+import (
+	"context"
+	"fmt"
+	"github.com/coroot/coroot-node-agent/proc"
+	"github.com/coroot/logparser"
+	"github.com/docker/docker/client"
+	"inet.af/netaddr"
+	"strings"
+	"time"
+)
+
+const dockerdTimeout = 30 * time.Second
+
+var (
+	dockerdClient *client.Client
+)
+
+func DockerdInit() error {
+	c, err := client.NewClientWithOpts(
+		client.WithHost("unix://"+proc.HostPath("/run/docker.sock")),
+		client.WithVersion("1.12"),
+	)
+	if err != nil {
+		return err
+	}
+	ctx, cancelFn := context.WithTimeout(context.Background(), dockerdTimeout)
+	defer cancelFn()
+	c.NegotiateAPIVersion(ctx)
+	dockerdClient = c
+	return nil
+}
+
+func DockerdInspect(containerID string) (*ContainerMetadata, error) {
+	if dockerdClient == nil {
+		return nil, fmt.Errorf("dockerd client not initialized")
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), dockerdTimeout)
+	defer cancel()
+	c, err := dockerdClient.ContainerInspect(ctx, containerID)
+	if err != nil {
+		return nil, err
+	}
+	res := &ContainerMetadata{
+		name:        strings.TrimPrefix(c.Name, "/"),
+		labels:      c.Config.Labels,
+		volumes:     map[string]Volume{},
+		hostListens: map[string][]netaddr.IPPort{},
+	}
+	for _, m := range c.Mounts {
+		if provisioner, volume := parseVolumeSource(m.Source); provisioner != "" && volume != "" {
+			res.volumes[m.Destination] = Volume{provisioner: provisioner, volume: volume}
+		}
+	}
+	if c.LogPath != "" && c.HostConfig.LogConfig.Type == "json-file" {
+		res.logPath = c.LogPath
+		res.logDecoder = logparser.DockerJsonDecoder{}
+	}
+	if c.NetworkSettings != nil {
+		addrs := map[netaddr.IPPort]struct{}{}
+		for port, bindings := range c.NetworkSettings.Ports {
+			if port.Proto() != "tcp" {
+				continue
+			}
+			for _, b := range bindings {
+				if ipp, err := netaddr.ParseIPPort(b.HostIP + ":" + b.HostPort); err == nil {
+					addrs[ipp] = struct{}{}
+				}
+			}
+		}
+		if len(addrs) > 0 {
+			s := make([]netaddr.IPPort, 0, len(addrs))
+			for addr := range addrs {
+				s = append(s, addr)
+			}
+			res.hostListens["dockerd"] = s
+		}
+	}
+	return res, nil
+}

+ 43 - 0
containers/journald.go

@@ -0,0 +1,43 @@
+package containers
+
+import (
+	"fmt"
+	"github.com/coroot/coroot-node-agent/cgroup"
+	"github.com/coroot/coroot-node-agent/logs"
+	"github.com/coroot/coroot-node-agent/proc"
+	"github.com/coroot/logparser"
+)
+
+var (
+	journaldReader *logs.JournaldReader
+)
+
+func JournaldInit() error {
+	r, err := logs.NewJournaldReader(
+		proc.HostPath("/run/log/journal"),
+		proc.HostPath("/var/log/journal"),
+	)
+	if err != nil {
+		return err
+	}
+	journaldReader = r
+	return nil
+}
+
+func JournaldSubscribe(cg *cgroup.Cgroup, ch chan<- logparser.LogEntry) error {
+	if journaldReader == nil {
+		return fmt.Errorf("journald reader not initialized")
+	}
+	err := journaldReader.Subscribe(cg.Id, ch)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func JournaldUnsubscribe(cg *cgroup.Cgroup) {
+	if journaldReader == nil {
+		return
+	}
+	journaldReader.Unsubscribe(cg.Id)
+}

+ 85 - 0
containers/metrics.go

@@ -0,0 +1,85 @@
+package containers
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+	"reflect"
+)
+
+var metrics = struct {
+	Restarts *prometheus.Desc
+
+	CPULimit      *prometheus.Desc
+	CPUUsage      *prometheus.Desc
+	CPUDelay      *prometheus.Desc
+	ThrottledTime *prometheus.Desc
+
+	MemoryLimit *prometheus.Desc
+	MemoryRss   *prometheus.Desc
+	MemoryCache *prometheus.Desc
+	OOMKills    *prometheus.Desc
+
+	DiskDelay      *prometheus.Desc
+	DiskSize       *prometheus.Desc
+	DiskUsed       *prometheus.Desc
+	DiskReserved   *prometheus.Desc
+	DiskReadOps    *prometheus.Desc
+	DiskReadBytes  *prometheus.Desc
+	DiskWriteOps   *prometheus.Desc
+	DiskWriteBytes *prometheus.Desc
+
+	NetListenInfo         *prometheus.Desc
+	NetConnectsSuccessful *prometheus.Desc
+	NetConnectsFailed     *prometheus.Desc
+	NetConnectionsActive  *prometheus.Desc
+	NetRetransmits        *prometheus.Desc
+	NetLatency            *prometheus.Desc
+
+	LogMessages *prometheus.Desc
+
+	ApplicationType *prometheus.Desc
+}{
+	Restarts: metric("container_restarts_total", "Number of times the container was restarted"),
+
+	CPULimit:      metric("container_resources_cpu_limit_cores", "CPU limit of the container"),
+	CPUUsage:      metric("container_resources_cpu_usage_seconds_total", "Total CPU time consumed by the container"),
+	CPUDelay:      metric("container_resources_cpu_delay_seconds_total", "Total time duration processes of the container have been waiting for a CPU (while being runnable)"),
+	ThrottledTime: metric("container_resources_cpu_throttled_seconds_total", "Total time duration the container has been throttled"),
+
+	MemoryLimit: metric("container_resources_memory_limit_bytes", "Memory limit of the container"),
+	MemoryRss:   metric("container_resources_memory_rss_bytes", "Amount of physical memory used by the container (doesn't include page cache)"),
+	MemoryCache: metric("container_resources_memory_cache_bytes", "Amount of page cache memory allocated by the container"),
+	OOMKills:    metric("container_oom_kills_total", "Total number of times the container was terminated by the OOM killer"),
+
+	DiskDelay:      metric("container_resources_disk_delay_seconds_total", "Total time duration processes of the container have been waiting fot I/Os to complete"),
+	DiskSize:       metric("container_resources_disk_size_bytes", "Total capacity of the volume", "mount_point", "device", "provisioner", "volume"),
+	DiskUsed:       metric("container_resources_disk_used_bytes", "Used capacity of the volume", "mount_point", "device", "provisioner", "volume"),
+	DiskReserved:   metric("container_resources_disk_reserved_bytes", "Reserved capacity of the volume", "mount_point", "device", "provisioner", "volume"),
+	DiskReadOps:    metric("container_resources_disk_reads_total", "Total number of reads completed successfully by the container", "mount_point", "device", "provisioner", "volume"),
+	DiskReadBytes:  metric("container_resources_disk_read_bytes_total", "Total number of bytes read from the disk by the container", "mount_point", "device", "provisioner", "volume"),
+	DiskWriteOps:   metric("container_resources_disk_writes_total", "Total number of writes completed successfully by the container", "mount_point", "device", "provisioner", "volume"),
+	DiskWriteBytes: metric("container_resources_disk_written_bytes_total", "Total number of bytes written to the disk by the container", "mount_point", "device", "provisioner", "volume"),
+
+	NetListenInfo:         metric("container_net_tcp_listen_info", "Listen address of the container", "listen_addr", "proxy"),
+	NetConnectsSuccessful: metric("container_net_tcp_successful_connects_total", "Total number of successful TCP connects", "destination", "actual_destination"),
+	NetConnectsFailed:     metric("container_net_tcp_failed_connects_total", "Total number of failed TCP connects", "destination"),
+	NetConnectionsActive:  metric("container_net_tcp_active_connections", "Number of active outbound connections used by the container", "destination", "actual_destination"),
+	NetRetransmits:        metric("container_net_tcp_retransmits_total", "Total number of retransmitted TCP segments", "destination", "actual_destination"),
+	NetLatency:            metric("container_net_latency_seconds", "Round-trip time between the container and a remote IP", "destination_ip"),
+
+	LogMessages: metric("container_log_messages_total", "Number of messages grouped by the automatically extracted repeated pattern", "source", "level", "pattern_hash", "sample"),
+
+	ApplicationType: metric("container_application_type", "Type of the application running in the container (e.g. memcached, postgres, mysql)", "application_type"),
+}
+
+func metric(name, help string, labels ...string) *prometheus.Desc {
+	return prometheus.NewDesc(name, help, labels, nil)
+}
+
+var metricsList []*prometheus.Desc
+
+func init() {
+	v := reflect.ValueOf(metrics)
+	for i := 0; i < v.NumField(); i++ {
+		metricsList = append(metricsList, v.Field(i).Interface().(*prometheus.Desc))
+	}
+}

+ 299 - 0
containers/registry.go

@@ -0,0 +1,299 @@
+package containers
+
+import (
+	"fmt"
+	"github.com/coroot/coroot-node-agent/cgroup"
+	"github.com/coroot/coroot-node-agent/common"
+	"github.com/coroot/coroot-node-agent/ebpftracer"
+	"github.com/coroot/coroot-node-agent/proc"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/vishvananda/netns"
+	"k8s.io/klog/v2"
+	"os"
+	"time"
+)
+
+var (
+	selfNetNs   = netns.None()
+	hostNetNsId = netns.None().UniqueId()
+	agentPid    = uint32(os.Getpid())
+)
+
+type Registry struct {
+	reg prometheus.Registerer
+
+	tracer *ebpftracer.Tracer
+	events chan ebpftracer.Event
+
+	containersById       map[ContainerID]*Container
+	containersByCgroupId map[string]*Container
+	containersByPid      map[uint32]*Container
+}
+
+func NewRegistry(reg prometheus.Registerer, kernelVersion string) (*Registry, error) {
+	ns, err := proc.GetSelfNetNs()
+	if err != nil {
+		return nil, err
+	}
+	selfNetNs = ns
+	hostNetNs, err := proc.GetHostNetNs()
+	if err != nil {
+		return nil, err
+	}
+	defer hostNetNs.Close()
+	hostNetNsId = hostNetNs.UniqueId()
+
+	err = proc.ExecuteInNetNs(hostNetNs, selfNetNs, func() error {
+		if err := TaskstatsInit(); err != nil {
+			return err
+		}
+		if err := ConntrackInit(); err != nil {
+			return err
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	if err := DockerdInit(); err != nil {
+		klog.Warningln(err)
+	}
+	if err := ContainerdInit(); err != nil {
+		klog.Warningln(err)
+	}
+	if err := JournaldInit(); err != nil {
+		klog.Warningln(err)
+	}
+
+	cs := &Registry{
+		reg:    reg,
+		events: make(chan ebpftracer.Event, 10000),
+
+		containersById:       map[ContainerID]*Container{},
+		containersByCgroupId: map[string]*Container{},
+		containersByPid:      map[uint32]*Container{},
+	}
+
+	go cs.handleEvents(cs.events)
+	t, err := ebpftracer.NewTracer(cs.events, kernelVersion)
+	if err != nil {
+		close(cs.events)
+		return nil, err
+	}
+	cs.tracer = t
+
+	return cs, nil
+}
+
+func (r *Registry) Close() {
+	r.tracer.Close()
+	close(r.events)
+}
+
+func (r *Registry) handleEvents(ch <-chan ebpftracer.Event) {
+	gcTicker := time.NewTicker(gcInterval)
+	defer gcTicker.Stop()
+	for {
+		select {
+		case now := <-gcTicker.C:
+			for id, c := range r.containersById {
+				if !c.Dead(now) {
+					continue
+				}
+				for cg, cc := range r.containersByCgroupId {
+					if cc == c {
+						delete(r.containersByCgroupId, cg)
+					}
+				}
+				for pid, cc := range r.containersByPid {
+					if cc == c {
+						delete(r.containersByPid, pid)
+					}
+				}
+				prometheus.WrapRegistererWith(prometheus.Labels{"container_id": string(id)}, r.reg).Unregister(c)
+				delete(r.containersById, id)
+				c.Close()
+			}
+
+		case e, more := <-ch:
+			if !more {
+				return
+			}
+			klog.Infoln(e)
+			switch e.Type {
+			case ebpftracer.EventTypeProcessStart:
+				c, seen := r.containersByPid[e.Pid]
+				switch { // possible pids wraparound + missed `process-exit` event
+				case c == nil && seen: // ignored
+					delete(r.containersByPid, e.Pid)
+					continue
+				case c != nil: // revalidating by cgroup
+					cg, err := proc.ReadCgroup(e.Pid)
+					if err != nil || cg.Id != c.cgroup.Id {
+						delete(r.containersByPid, e.Pid)
+						c.onProcessExit(e.Pid, false)
+					}
+				}
+				if c := r.getOrCreateContainer(e.Pid); c != nil {
+					c.onProcessStart(e.Pid)
+				}
+			case ebpftracer.EventTypeProcessExit:
+				if c := r.containersByPid[e.Pid]; c != nil {
+					c.onProcessExit(e.Pid, e.Reason == ebpftracer.EventReasonOOMKill)
+				}
+				delete(r.containersByPid, e.Pid)
+
+			case ebpftracer.EventTypeFileOpen:
+				if c := r.getOrCreateContainer(e.Pid); c != nil {
+					c.onFileOpen(e.Pid, e.Fd)
+				}
+
+			case ebpftracer.EventTypeListenOpen:
+				if c := r.getOrCreateContainer(e.Pid); c != nil {
+					c.onListenOpen(e.Pid, e.SrcAddr)
+				} else {
+					klog.Infoln("TCP listen open from unknown container", e)
+				}
+			case ebpftracer.EventTypeListenClose:
+				if c := r.containersByPid[e.Pid]; c != nil {
+					c.onListenClose(e.Pid, e.SrcAddr)
+				}
+
+			case ebpftracer.EventTypeConnectionOpen:
+				if c := r.getOrCreateContainer(e.Pid); c != nil {
+					c.onConnectionOpen(e.Pid, e.SrcAddr, e.DstAddr, false)
+				} else {
+					klog.Infoln("TCP connection from unknown container", e)
+				}
+			case ebpftracer.EventTypeConnectionError:
+				if c := r.getOrCreateContainer(e.Pid); c != nil {
+					c.onConnectionOpen(e.Pid, e.SrcAddr, e.DstAddr, true)
+				} else {
+					klog.Infoln("TCP connection error from unknown container", e)
+				}
+			case ebpftracer.EventTypeConnectionClose:
+				srcDst := AddrPair{src: e.SrcAddr, dst: e.DstAddr}
+				for _, c := range r.containersById {
+					if c.onConnectionClose(srcDst) {
+						break
+					}
+				}
+			case ebpftracer.EventTypeTCPRetransmit:
+				srcDst := AddrPair{src: e.SrcAddr, dst: e.DstAddr}
+				for _, c := range r.containersById {
+					if c.onRetransmit(srcDst) {
+						break
+					}
+				}
+			}
+		}
+	}
+}
+
+func (r *Registry) getOrCreateContainer(pid uint32) *Container {
+	if c, seen := r.containersByPid[pid]; c != nil {
+		klog.Infof("got container by pid %d -> %s", pid, c.cgroup.Id)
+		return c
+	} else if seen { // ignored
+		klog.Infof("ignored container for pid %d", pid)
+		return nil
+	}
+	cg, err := proc.ReadCgroup(pid)
+	if err != nil {
+		if !common.IsNotExist(err) {
+			klog.Warningln("failed to read proc cgroup:", err)
+		}
+		return nil
+	}
+	klog.Infof("got cgroup by pid %d -> %s", pid, cg.Id)
+	if c := r.containersByCgroupId[cg.Id]; c != nil {
+		klog.Infof("found container by cgroup pid %d -> %s", pid, cg.Id)
+		r.containersByPid[pid] = c
+		return c
+	}
+	md, err := getContainerMetadata(cg)
+	if err != nil {
+		klog.Warningln(err)
+		return nil
+	}
+	id := calcId(cg, md)
+	klog.Infof("calculated container id  %d -> %s -> %s", pid, cg.Id, id)
+	if id == "" {
+		if cg.Id == "/init.scope" && pid != 1 {
+			klog.InfoS("ignoring without persisting", "cg", cg.Id, "pid", pid)
+		} else {
+			klog.InfoS("ignoring", "cg", cg.Id, "pid", pid)
+			r.containersByPid[pid] = nil
+		}
+		return nil
+	}
+	if c := r.containersById[id]; c != nil {
+		klog.Warningln("id conflict:", id)
+		if cg.CreatedAt().After(c.cgroup.CreatedAt()) {
+			c.cgroup = cg
+			c.metadata = md
+			c.runLogParser("")
+		}
+		r.containersByPid[pid] = c
+		r.containersByCgroupId[cg.Id] = c
+		return c
+	}
+	c := NewContainer(cg, md)
+	klog.InfoS("detected container", "pid", pid, "cg", cg.Id, "id", id)
+	if err := prometheus.WrapRegistererWith(prometheus.Labels{"container_id": string(id)}, r.reg).Register(c); err != nil {
+		klog.Warningln(err)
+		return nil
+	}
+	r.containersByPid[pid] = c
+	r.containersByCgroupId[cg.Id] = c
+	r.containersById[id] = c
+	return c
+}
+
+func calcId(cg *cgroup.Cgroup, md *ContainerMetadata) ContainerID {
+	if cg.ContainerType == cgroup.ContainerTypeSystemdService {
+		return ContainerID(cg.ContainerId)
+	}
+	if cg.ContainerType != cgroup.ContainerTypeDocker {
+		return ""
+	}
+	if md.labels["io.kubernetes.pod.name"] != "" {
+		pod := md.labels["io.kubernetes.pod.name"]
+		namespace := md.labels["io.kubernetes.pod.namespace"]
+		name := md.labels["io.kubernetes.container.name"]
+		if name == "" || name == "POD" { // skip pause|sandbox containers
+			return ""
+		}
+		return ContainerID(fmt.Sprintf("/k8s/%s/%s/%s", namespace, pod, name))
+	}
+	if md.name == "" { // should be "pure" dockerd container here
+		klog.Warningln("empty dockerd container name for:", cg.ContainerId)
+		return ""
+	}
+	return ContainerID("/docker/" + md.name)
+}
+
+func getContainerMetadata(cg *cgroup.Cgroup) (*ContainerMetadata, error) {
+	if cg.ContainerType != cgroup.ContainerTypeDocker {
+		return &ContainerMetadata{}, nil
+	}
+	var dockerdErr error
+	if dockerdClient != nil {
+		md, err := DockerdInspect(cg.ContainerId)
+		if err == nil {
+			return md, nil
+		}
+		klog.Warningln("failed to inspect container %s: %s", cg.ContainerId, err)
+		dockerdErr = err
+	}
+	var containerdErr error
+	if containerdClient != nil {
+		md, err := ContainerdInspect(cg.ContainerId)
+		if err == nil {
+			return md, nil
+		}
+		klog.Warningln("failed to inspect container %s: %s", cg.ContainerId, err)
+		containerdErr = err
+	}
+	return nil, fmt.Errorf("failed to interact with dockerd (%s) or with containerd (%s)", dockerdErr, containerdErr)
+}

+ 47 - 0
containers/taskstats.go

@@ -0,0 +1,47 @@
+package containers
+
+import (
+	"fmt"
+	"github.com/mdlayher/taskstats"
+	"sync"
+)
+
+var (
+	taskstatsClient *taskstats.Client
+	taskstatsLock   sync.Mutex
+)
+
+func TaskstatsInit() error {
+	c, err := taskstats.New()
+	if err != nil {
+		return err
+	}
+	taskstatsClient = c
+	return nil
+}
+
+func TaskstatsTGID(pid uint32) (*taskstats.Stats, error) {
+	if taskstatsClient == nil {
+		return nil, fmt.Errorf("taskstats client not initialized")
+	}
+	taskstatsLock.Lock()
+	defer taskstatsLock.Unlock()
+	s, err := taskstatsClient.TGID(int(pid))
+	if err != nil {
+		return nil, err
+	}
+	return s, nil
+}
+
+func TaskstatsPID(pid uint32) (*taskstats.Stats, error) {
+	if taskstatsClient == nil {
+		return nil, fmt.Errorf("taskstats client not initialized")
+	}
+	taskstatsLock.Lock()
+	defer taskstatsLock.Unlock()
+	s, err := taskstatsClient.PID(int(pid))
+	if err != nil {
+		return nil, err
+	}
+	return s, nil
+}

+ 20 - 0
containers/volumes.go

@@ -0,0 +1,20 @@
+package containers
+
+import "regexp"
+
+var (
+	k8sVolumeDir = regexp.MustCompile(`.+/volumes/kubernetes.io~([^/]+)/([^/]+)`)
+)
+
+func parseVolumeSource(source string) (string, string) {
+	groups := k8sVolumeDir.FindStringSubmatch(source)
+	if len(groups) != 3 {
+		return "", ""
+	}
+	provisioner, volume := groups[1], groups[2]
+	switch provisioner {
+	case "secret", "configmap", "empty-dir":
+		return "", ""
+	}
+	return provisioner, volume
+}

+ 16 - 0
ebpftracer/Dockerfile

@@ -0,0 +1,16 @@
+FROM alpine:3.13
+
+RUN apk add llvm clang libbpf-dev linux-headers
+
+COPY ebpf/* /tmp/
+WORKDIR /tmp
+
+RUN clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -D__KERNEL=416 -c ebpf.c -o ebpf416.o && llvm-strip --strip-debug ebpf416.o
+RUN clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -D__KERNEL=420 -c ebpf.c -o ebpf420.o && llvm-strip --strip-debug ebpf420.o
+RUN clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -D__KERNEL=506 -c ebpf.c -o ebpf506.o && llvm-strip --strip-debug ebpf506.o
+
+RUN echo -en '// generated - do not edit\npackage ebpftracer\nvar ebpfProg = []struct{v string; p []byte}{\n' > ebpf.go \
+	&& echo -en '\t{"v5.6", []byte("' >> ebpf.go && hexdump -v -e '"\x" 1/1 "%02x"' ebpf506.o >> ebpf.go && echo '")},' >> ebpf.go \
+	&& echo -en '\t{"v4.20", []byte("' >> ebpf.go && hexdump -v -e '"\x" 1/1 "%02x"' ebpf420.o >> ebpf.go && echo '")},' >> ebpf.go \
+	&& echo -en '\t{"v4.16", []byte("' >> ebpf.go && hexdump -v -e '"\x" 1/1 "%02x"' ebpf416.o >> ebpf.go && echo '")},' >> ebpf.go \
+	&& echo -en '}\n'>> ebpf.go

+ 31 - 0
ebpftracer/Makefile

@@ -0,0 +1,31 @@
+build:
+	@echo ===BUILDING===
+	docker build -t ebpftracer .
+	docker cp $(shell docker create --rm ebpftracer):/tmp/ebpf.go ./ebpf.go
+	@echo
+
+test: test_vm1 test_vm2 test_vm3
+
+define test_in_vm
+	@echo ===TESTING IN $(1)===
+	vagrant ssh $(1) -c "uname -r && cd /tmp/src && sudo go test -p 1 -count 1 -v ./ebpftracer/..."
+	@echo
+endef
+
+test_vm1: build
+	$(call test_in_vm,ubuntu1810)
+
+test_vm2: build
+	$(call test_in_vm,ubuntu2004)
+
+test_vm3: build
+	$(call test_in_vm,ubuntu2010)
+
+vms_start:
+	vagrant up
+
+vms_stop:
+	vagrant suspend
+
+vms_delete:
+	vagrant destroy

+ 31 - 0
ebpftracer/Vagrantfile

@@ -0,0 +1,31 @@
+$script = <<-SCRIPT
+sudo apt update && sudo apt -y install gcc cgroup-tools
+sudo snap install go --classic
+
+# prevent unexpected `systemd-udevd` processes from appearing
+systemctl stop systemd-udevd.service systemd-udevd-control.socket systemd-udevd-kernel.socket
+
+sudo swapoff -a
+sudo sysctl vm.overcommit_memory=1
+SCRIPT
+
+Vagrant.configure("2") do |config|
+	config.vm.provider "virtualbox" do |v|
+  		v.memory = 1024
+  		v.cpus = 2
+	end
+
+	config.vm.provision "shell", inline: $script
+	config.vm.box_check_update = false
+	config.vm.synced_folder "..", "/tmp/src"
+
+	config.vm.define "ubuntu1810" do |ubuntu1810|
+		ubuntu1810.vm.box = "generic/ubuntu1810"
+    end
+	config.vm.define "ubuntu2004" do |ubuntu2004|
+		ubuntu2004.vm.box = "generic/ubuntu2004"
+    end
+	config.vm.define "ubuntu2010" do |ubuntu2010|
+		ubuntu2010.vm.box = "generic/ubuntu2010"
+    end
+end

Fișier diff suprimat deoarece este prea mare
+ 3 - 0
ebpftracer/ebpf.go


+ 23 - 0
ebpftracer/ebpf/ebpf.c

@@ -0,0 +1,23 @@
+#include <uapi/linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+#include <bpf/bpf_tracing.h>
+
+#define EVENT_TYPE_PROCESS_START	1
+#define EVENT_TYPE_PROCESS_EXIT		2
+#define EVENT_TYPE_CONNECTION_OPEN	3
+#define EVENT_TYPE_CONNECTION_CLOSE	4
+#define EVENT_TYPE_CONNECTION_ERROR	5
+#define EVENT_TYPE_LISTEN_OPEN		6
+#define EVENT_TYPE_LISTEN_CLOSE 	7
+#define EVENT_TYPE_FILE_OPEN		8
+#define EVENT_TYPE_TCP_RETRANSMIT	9
+
+#define EVENT_REASON_OOM_KILL		1
+
+#include "proc.c"
+#include "file.c"
+#include "tcp_state.c"
+#include "tcp_retransmit.c"
+
+char _license[] SEC("license") = "GPL";

+ 100 - 0
ebpftracer/ebpf/file.c

@@ -0,0 +1,100 @@
+#include <asm-generic/fcntl.h>
+
+struct file_event {
+	__u32 type;
+	__u32 pid;
+	__u32 fd;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+	__uint(key_size, sizeof(int));
+	__uint(value_size, sizeof(int));
+} file_events SEC(".maps");
+
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(key_size, sizeof(__u64));
+	__uint(value_size, sizeof(__u32));
+	__uint(max_entries, 10240);
+} open_file_info SEC(".maps");
+
+struct trace_event_raw_sys_enter__stub {
+	__u64 unused;
+	long int id;
+	long unsigned int args[6];
+};
+
+struct trace_event_raw_sys_exit__stub {
+	__u64 unused;
+	long int id;
+	long int ret;
+};
+
+static __always_inline
+int trace_enter(struct trace_event_raw_sys_enter__stub* ctx, int at)
+{
+	int flags = (int)ctx->args[at+1];
+	if (!(flags & O_ACCMODE & (O_WRONLY | O_RDWR))) {
+		return 0;
+	}
+	char p[7];
+	long res = bpf_probe_read_str(&p, sizeof(p), (void *)ctx->args[at]);
+	if (p[0]=='/' && p[1]=='p' && p[2]=='r' && p[3]=='o' && p[4]=='c' && p[5]=='/') {
+		return 0;
+	}
+	if (p[0]=='/' && p[1]=='d' && p[2]=='e' && p[3]=='v' && p[4]=='/') {
+		return 0;
+	}
+	if (p[0]=='/' && p[1]=='s' && p[2]=='y' && p[3]=='s' && p[4]=='/') {
+		return 0;
+	}
+	__u64 id = bpf_get_current_pid_tgid();
+	__u32 v = 1;
+	bpf_map_update_elem(&open_file_info, &id, &v, BPF_ANY);
+	return 0;
+}
+
+static __always_inline
+int trace_exit(struct trace_event_raw_sys_exit__stub* ctx)
+{
+	__u64 id = bpf_get_current_pid_tgid();
+	if (!bpf_map_lookup_elem(&open_file_info, &id)) {
+		return 0;
+	}
+	bpf_map_delete_elem(&open_file_info, &id);
+	if (ctx->ret < 0) {
+		return 0;
+	}
+	struct file_event e = {
+		.type = EVENT_TYPE_FILE_OPEN,
+		.pid = id >> 32,
+		.fd = ctx->ret,
+	};
+	bpf_perf_event_output(ctx, &file_events, BPF_F_CURRENT_CPU, &e, sizeof(e));
+	return 0;
+}
+
+SEC("tracepoint/syscalls/sys_enter_open")
+int sys_enter_open(struct trace_event_raw_sys_enter__stub* ctx)
+{
+	return trace_enter(ctx, 0);
+}
+
+SEC("tracepoint/syscalls/sys_exit_open")
+int sys_exit_open(struct trace_event_raw_sys_exit__stub* ctx)
+{
+	return trace_exit(ctx);
+}
+
+SEC("tracepoint/syscalls/sys_enter_openat")
+int sys_enter_openat(struct trace_event_raw_sys_enter__stub* ctx)
+{
+	return trace_enter(ctx, 1);
+}
+
+SEC("tracepoint/syscalls/sys_exit_openat")
+int sys_exit_openat(struct trace_event_raw_sys_exit__stub* ctx)
+{
+	return trace_exit(ctx);
+}

+ 80 - 0
ebpftracer/ebpf/proc.c

@@ -0,0 +1,80 @@
+#define TASK_COMM_LEN	16
+#define CLONE_THREAD 	0x00010000
+
+struct proc_event {
+	__u32 type;
+	__u32 pid;
+	__u32 reason;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+	__uint(key_size, sizeof(int));
+	__uint(value_size, sizeof(int));
+} proc_events SEC(".maps");
+
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(key_size, sizeof(__u32));
+	__uint(value_size, sizeof(__u32));
+	__uint(max_entries, 10240);
+} oom_info SEC(".maps");
+
+struct trace_event_raw_task_newtask__stub {
+	__u64 unused;
+	__u32 pid;
+	char comm[TASK_COMM_LEN];
+	long unsigned int clone_flags;
+};
+
+SEC("tracepoint/task/task_newtask")
+int task_newtask(struct trace_event_raw_task_newtask__stub *args)
+{
+	if (args->clone_flags & CLONE_THREAD) { // skipping threads
+		return 0;
+	}
+	struct proc_event e = {
+		.type = EVENT_TYPE_PROCESS_START,
+		.pid = args->pid,
+	};
+	bpf_perf_event_output(args, &proc_events, BPF_F_CURRENT_CPU, &e, sizeof(e));
+	return 0;
+}
+
+struct trace_event_raw_sched_process_template__stub {
+	__u64 unused;
+	char comm[TASK_COMM_LEN];
+	__u32 pid;
+};
+
+SEC("tracepoint/sched/sched_process_exit")
+int sched_process_exit(struct trace_event_raw_sched_process_template__stub *args)
+{
+	__u64 id = bpf_get_current_pid_tgid();
+	if (id >> 32 != (__u32)id) { // skipping threads
+		return 0;
+	}
+	struct proc_event e = {
+		.type = EVENT_TYPE_PROCESS_EXIT,
+		.pid = args->pid,
+	};
+	if (bpf_map_lookup_elem(&oom_info, &e.pid)) {
+		e.reason = EVENT_REASON_OOM_KILL;
+		bpf_map_delete_elem(&oom_info, &e.pid);
+	}
+	bpf_perf_event_output(args, &proc_events, BPF_F_CURRENT_CPU, &e, sizeof(e));
+	return 0;
+}
+
+struct trace_event_raw_mark_victim__stub {
+	__u64 unused;
+	int pid;
+};
+
+SEC("tracepoint/oom/mark_victim")
+int oom_mark_victim(struct trace_event_raw_mark_victim__stub *args)
+{
+	__u32 pid = args->pid;
+	bpf_map_update_elem(&oom_info, &pid, &pid, BPF_ANY);
+	return 0;
+}

+ 36 - 0
ebpftracer/ebpf/tcp_retransmit.c

@@ -0,0 +1,36 @@
+struct {
+	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+	__uint(key_size, sizeof(int));
+	__uint(value_size, sizeof(int));
+} tcp_retransmit_events SEC(".maps");
+
+struct trace_event_raw_tcp_event_sk_skb__stub {
+	__u64 unused;
+	void *sbkaddr;
+	void *skaddr;
+#if __KERNEL >= 420
+	int state;
+#endif
+	__u16 sport;
+	__u16 dport;
+	__u8 saddr[4];
+	__u8 daddr[4];
+	__u8 saddr_v6[16];
+	__u8 daddr_v6[16];
+};
+
+SEC("tracepoint/tcp/tcp_retransmit_skb")
+int tcp_retransmit_skb(struct trace_event_raw_tcp_event_sk_skb__stub *args)
+{
+	struct tcp_event e = {
+		.type = EVENT_TYPE_TCP_RETRANSMIT,
+		.sport = args->sport,
+		.dport = args->dport,
+	};
+	__builtin_memcpy(&e.saddr, &args->saddr_v6, sizeof(e.saddr));
+	__builtin_memcpy(&e.daddr, &args->daddr_v6, sizeof(e.daddr));
+
+	bpf_perf_event_output(args, &tcp_retransmit_events, BPF_F_CURRENT_CPU, &e, sizeof(e));
+
+	return 0;
+}

+ 118 - 0
ebpftracer/ebpf/tcp_state.c

@@ -0,0 +1,118 @@
+#define IPPROTO_TCP 6
+
+struct tcp_event {
+	__u32 type;
+	__u32 pid;
+	__u16 sport;
+	__u16 dport;
+	__u8 saddr[16];
+	__u8 daddr[16];
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+	__uint(key_size, sizeof(int));
+	__uint(value_size, sizeof(int));
+} tcp_listen_events SEC(".maps");
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+	__uint(key_size, sizeof(int));
+	__uint(value_size, sizeof(int));
+} tcp_connect_events SEC(".maps");
+
+struct trace_event_raw_inet_sock_set_state__stub {
+	__u64 unused;
+	void *skaddr;
+	int oldstate;
+	int newstate;
+	__u16 sport;
+	__u16 dport;
+	__u16 family;
+#if __KERNEL >= 506
+	__u16 protocol;
+#else
+	__u8 protocol;
+#endif
+	__u8 saddr[4];
+	__u8 daddr[4];
+	__u8 saddr_v6[16];
+	__u8 daddr_v6[16];
+};
+
+struct sk_info {
+	__u32 pid;
+//	__u64 ts;
+};
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(key_size, sizeof(void *));
+	__uint(value_size, sizeof(struct sk_info));
+	__uint(max_entries, 10240);
+} sk_info SEC(".maps");
+
+SEC("tracepoint/sock/inet_sock_set_state")
+int inet_sock_set_state(void *ctx)
+{
+	struct trace_event_raw_inet_sock_set_state__stub args = {};
+	if (bpf_probe_read(&args, sizeof(args), ctx) < 0) {
+		return 0;
+	}
+	if (args.protocol != IPPROTO_TCP) {
+		return 0;
+	}
+	if (args.oldstate == BPF_TCP_CLOSE && args.newstate == BPF_TCP_SYN_SENT) {
+		struct sk_info i = {};
+		i.pid = bpf_get_current_pid_tgid() >> 32;
+		bpf_map_update_elem(&sk_info, &args.skaddr, &i, BPF_ANY);
+		return 0;
+	}
+
+	__u32 pid = bpf_get_current_pid_tgid() >> 32;
+	__u32 type = 0;
+	void *map = &tcp_connect_events;
+	if (args.oldstate == BPF_TCP_SYN_SENT) {
+		if (args.newstate == BPF_TCP_ESTABLISHED) {
+			type = EVENT_TYPE_CONNECTION_OPEN;
+		} else if (args.newstate == BPF_TCP_CLOSE) {
+			type = EVENT_TYPE_CONNECTION_ERROR;
+		} else {
+			return 0;
+		}
+		struct sk_info *i = bpf_map_lookup_elem(&sk_info, &args.skaddr);
+		if (!i) {
+			return 0;
+		}
+		pid = i->pid;
+		bpf_map_delete_elem(&sk_info, &args.skaddr);
+	}
+	if (args.oldstate == BPF_TCP_ESTABLISHED && (args.newstate == BPF_TCP_FIN_WAIT1 || args.newstate == BPF_TCP_CLOSE_WAIT)) {
+		pid = 0;
+		type = EVENT_TYPE_CONNECTION_CLOSE;
+	}
+	if (args.oldstate == BPF_TCP_CLOSE && args.newstate == BPF_TCP_LISTEN) {
+		type = EVENT_TYPE_LISTEN_OPEN;
+		map = &tcp_listen_events;
+	}
+	if (args.oldstate == BPF_TCP_LISTEN && args.newstate == BPF_TCP_CLOSE) {
+		type = EVENT_TYPE_LISTEN_CLOSE;
+		map = &tcp_listen_events;
+	}
+
+	if (type == 0) {
+		return 0;
+	}
+
+	struct tcp_event e = {
+		.type = type,
+		.pid = pid,
+		.sport = args.sport,
+		.dport = args.dport,
+	};
+	__builtin_memcpy(&e.saddr, &args.saddr_v6, sizeof(e.saddr));
+	__builtin_memcpy(&e.daddr, &args.daddr_v6, sizeof(e.saddr));
+
+	bpf_perf_event_output(ctx, map, BPF_F_CURRENT_CPU, &e, sizeof(e));
+
+	return 0;
+}

+ 70 - 0
ebpftracer/init.go

@@ -0,0 +1,70 @@
+package ebpftracer
+
+import (
+	"github.com/coroot/coroot-node-agent/proc"
+	"k8s.io/klog/v2"
+	"os"
+	"path"
+	"strconv"
+	"strings"
+)
+
+type fd struct {
+	pid uint32
+	fd  uint32
+}
+
+type sock struct {
+	pid uint32
+	proc.Sock
+}
+
+func readFds(pids []uint32) (fds []fd, socks []sock) {
+	nss := map[string]map[string]sock{}
+	for _, pid := range pids {
+		ns, err := proc.GetNetNs(pid)
+		if err != nil {
+			continue
+		}
+		nsId := ns.UniqueId()
+		sockets, ok := nss[nsId]
+		_ = ns.Close()
+		if !ok {
+			sockets = map[string]sock{}
+			nss[nsId] = sockets
+			if ss, err := proc.GetSockets(pid); err != nil {
+				klog.Warningln(err)
+			} else {
+				for _, s := range ss {
+					sockets[s.Inode] = sock{Sock: s}
+				}
+			}
+		}
+
+		fdDir := proc.Path(pid, "fd")
+		entries, err := os.ReadDir(fdDir)
+		if err != nil {
+			continue
+		}
+		for _, entry := range entries {
+			dest, err := os.Readlink(path.Join(fdDir, entry.Name()))
+			if err != nil {
+				continue
+			}
+			switch {
+			case strings.HasPrefix(dest, "socket:[") && strings.HasSuffix(dest, "]"):
+				inode := dest[len("socket:[") : len(dest)-1]
+				if s, ok := sockets[inode]; ok {
+					s.pid = pid
+					socks = append(socks, s)
+				}
+			default:
+				i, err := strconv.Atoi(entry.Name())
+				if err == nil {
+					fds = append(fds, fd{pid: pid, fd: uint32(i)})
+				}
+			}
+		}
+	}
+	return
+}

+ 265 - 0
ebpftracer/tracer.go

@@ -0,0 +1,265 @@
+package ebpftracer
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/link"
+	"github.com/cilium/ebpf/perf"
+	"github.com/coroot/coroot-node-agent/common"
+	"github.com/coroot/coroot-node-agent/proc"
+	"golang.org/x/mod/semver"
+	"golang.org/x/sys/unix"
+	"inet.af/netaddr"
+	"k8s.io/klog/v2"
+	"os"
+	"strconv"
+	"strings"
+)
+
+type EventType uint32
+type EventReason uint32
+
+const (
+	EventTypeProcessStart    EventType = 1
+	EventTypeProcessExit     EventType = 2
+	EventTypeConnectionOpen  EventType = 3
+	EventTypeConnectionClose EventType = 4
+	EventTypeConnectionError EventType = 5
+	EventTypeListenOpen      EventType = 6
+	EventTypeListenClose     EventType = 7
+	EventTypeFileOpen        EventType = 8
+	EventTypeTCPRetransmit   EventType = 9
+
+	EventReasonOOMKill EventReason = 1
+)
+
+type Event struct {
+	Type    EventType
+	Reason  EventReason
+	Pid     uint32
+	SrcAddr netaddr.IPPort
+	DstAddr netaddr.IPPort
+	Fd      uint32
+}
+
+type Tracer struct {
+	collection *ebpf.Collection
+	readers    map[string]*perf.Reader
+	links      []link.Link
+}
+
+func NewTracer(events chan<- Event, kernelVersion string) (*Tracer, error) {
+	t := &Tracer{readers: map[string]*perf.Reader{}}
+	if err := t.ebpf(events, kernelVersion); err != nil {
+		return nil, err
+	}
+	if err := t.init(events); err != nil {
+		return nil, err
+	}
+	return t, nil
+}
+
+func (t *Tracer) Close() {
+	for _, l := range t.links {
+		l.Close()
+	}
+	for _, r := range t.readers {
+		r.Close()
+	}
+	t.collection.Close()
+}
+
+func (t *Tracer) init(ch chan<- Event) error {
+	pids, err := proc.ListPids()
+	if err != nil {
+		return fmt.Errorf("failed to list pids: %w", err)
+	}
+	for _, pid := range pids {
+		ch <- Event{Type: EventTypeProcessStart, Pid: pid}
+	}
+
+	fds, sockets := readFds(pids)
+	for _, fd := range fds {
+		ch <- Event{Type: EventTypeFileOpen, Pid: fd.pid, Fd: fd.fd}
+	}
+
+	listens := map[uint64]bool{}
+	for _, s := range sockets {
+		if s.Listen {
+			listens[uint64(s.pid)<<32|uint64(s.SAddr.Port())] = true
+		}
+	}
+
+	for _, s := range sockets {
+		typ := EventTypeConnectionOpen
+		if s.Listen {
+			typ = EventTypeListenOpen
+		} else if listens[uint64(s.pid)<<32|uint64(s.SAddr.Port())] || s.DAddr.Port() > s.SAddr.Port() { // inbound
+			continue
+		}
+		ch <- Event{
+			Type:    typ,
+			Pid:     s.pid,
+			SrcAddr: s.SAddr,
+			DstAddr: s.DAddr,
+		}
+	}
+	return nil
+}
+
+func (t *Tracer) ebpf(ch chan<- Event, kernelVersion string) error {
+	kv := "v" + common.KernelMajorMinor(kernelVersion)
+	var prg []byte
+	for _, p := range ebpfProg {
+		if semver.Compare(kv, p.v) >= 0 {
+			prg = p.p
+			break
+		}
+	}
+	if len(prg) == 0 {
+		return fmt.Errorf("unsupported kernel version: %s", kernelVersion)
+	}
+	spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(prg))
+	if err != nil {
+		return fmt.Errorf("failed to load spec: %w", err)
+	}
+	_ = unix.Setrlimit(unix.RLIMIT_MEMLOCK, &unix.Rlimit{Cur: unix.RLIM_INFINITY, Max: unix.RLIM_INFINITY})
+	c, err := ebpf.NewCollection(spec)
+	if err != nil {
+		return fmt.Errorf("failed to load collection: %w", err)
+	}
+	t.collection = c
+
+	events := map[string]rawEvent{
+		"proc_events":           &procEvent{},
+		"tcp_listen_events":     &tcpEvent{},
+		"tcp_connect_events":    &tcpEvent{},
+		"tcp_retransmit_events": &tcpEvent{},
+		"file_events":           &fileEvent{},
+	}
+	for name, typ := range events {
+		r, err := perf.NewReader(t.collection.Maps[name], os.Getpagesize())
+		if err != nil {
+			t.Close()
+			return fmt.Errorf("failed to create ebpf reader: %w", err)
+		}
+		t.readers[name] = r
+		go runEventsReader(name, r, ch, typ)
+	}
+
+	for name, spec := range spec.Programs {
+		p := t.collection.Programs[name]
+		var err error
+		var l link.Link
+		switch spec.Type {
+		case ebpf.TracePoint:
+			parts := strings.SplitN(spec.AttachTo, "/", 2)
+			l, err = link.Tracepoint(parts[0], parts[1], p)
+		case ebpf.Kprobe:
+			l, err = link.Kprobe(spec.AttachTo, p)
+		}
+		if err != nil {
+			t.Close()
+			return fmt.Errorf("failed to link program: %w", err)
+		}
+		t.links = append(t.links, l)
+	}
+
+	return nil
+}
+
+func (t EventType) String() string {
+	switch t {
+	case EventTypeProcessStart:
+		return "process-start"
+	case EventTypeProcessExit:
+		return "process-exit"
+	case EventTypeConnectionOpen:
+		return "connection-open"
+	case EventTypeConnectionClose:
+		return "connection-close"
+	case EventTypeConnectionError:
+		return "connection-error"
+	case EventTypeListenOpen:
+		return "listen-open"
+	case EventTypeListenClose:
+		return "listen-close"
+	case EventTypeFileOpen:
+		return "file-open"
+	case EventTypeTCPRetransmit:
+		return "tcp-retransmit"
+	}
+	return "unknown: " + strconv.Itoa(int(t))
+}
+
+func (t EventReason) String() string {
+	switch t {
+	case EventReasonOOMKill:
+		return "oom-kill"
+	}
+	return "unknown: " + strconv.Itoa(int(t))
+}
+
+type rawEvent interface {
+	Event() Event
+}
+
+type procEvent struct {
+	Type   uint32
+	Pid    uint32
+	Reason uint32
+}
+
+func (e procEvent) Event() Event {
+	return Event{Type: EventType(e.Type), Reason: EventReason(e.Reason), Pid: e.Pid}
+}
+
+type tcpEvent struct {
+	Type  uint32
+	Pid   uint32
+	SPort uint16
+	DPort uint16
+	SAddr [16]byte
+	DAddr [16]byte
+}
+
+func (e tcpEvent) Event() Event {
+	return Event{Type: EventType(e.Type), Pid: e.Pid, SrcAddr: ipPort(e.SAddr, e.SPort), DstAddr: ipPort(e.DAddr, e.DPort)}
+}
+
+type fileEvent struct {
+	Type uint32
+	Pid  uint32
+	Fd   uint32
+}
+
+func (e fileEvent) Event() Event {
+	return Event{Type: EventType(e.Type), Pid: e.Pid, Fd: e.Fd}
+}
+
+func runEventsReader(name string, r *perf.Reader, ch chan<- Event, e rawEvent) {
+	for {
+		rec, err := r.Read()
+		if err != nil {
+			if perf.IsClosed(err) {
+				break
+			}
+			continue
+		}
+		if rec.LostSamples > 0 {
+			klog.Errorln(name, "lost samples:", rec.LostSamples)
+		}
+		if err := binary.Read(bytes.NewBuffer(rec.RawSample), binary.LittleEndian, e); err != nil {
+			klog.Warningln("failed to read msg:", err)
+			continue
+		}
+		ch <- e.Event()
+	}
+}
+
+func ipPort(ip [16]byte, port uint16) netaddr.IPPort {
+	i, _ := netaddr.FromStdIP(ip[:])
+	return netaddr.IPPortFrom(i, port)
+}

+ 336 - 0
ebpftracer/tracer_test.go

@@ -0,0 +1,336 @@
+package ebpftracer
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/containerd/cgroups"
+	"github.com/opencontainers/runtime-spec/specs-go"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/sys/unix"
+	"net"
+	"os"
+	"os/exec"
+	"path"
+	"strconv"
+	"strings"
+	"syscall"
+	"testing"
+	"time"
+)
+
+func skipIfNotRoot(t *testing.T) {
+	if os.Getuid() != 0 {
+		t.SkipNow()
+	}
+}
+
+func TestProcessEvents(t *testing.T) {
+	skipIfNotRoot(t)
+	src := `
+		package main
+		
+		import (
+			"bytes"
+			"os"
+			"strconv"
+			"time"
+		)
+		
+		func main() {
+			mb, _ := strconv.Atoi(os.Args[1])
+			sleep, _ := time.ParseDuration(os.Args[2])
+			bytes.Repeat([]byte("x"), mb*1024*1024)
+			time.Sleep(sleep)
+		}
+	`
+	program := path.Join(t.TempDir(), "program")
+	require.NoError(t, os.WriteFile(program+".go", []byte(src), 0644))
+	require.NoError(t, exec.Command("go", "build", "-o", program, program+".go").Run())
+
+	getEvent, stop := runTracer(t, false)
+	defer stop()
+	for {
+		if e := getEvent(); e == nil {
+			break
+		}
+	}
+
+	p1 := exec.Command(program, "600", "10s")
+	require.NoError(t, p1.Start())
+	time.Sleep(time.Second)
+	assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: uint32(p1.Process.Pid)}, *getEvent())
+
+	// p1 should be killed by the OOM killer, because VM have only 1 GB of memory total
+	p2 := exec.Command(program, "400", "1s")
+	require.NoError(t, p2.Run())
+	assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: uint32(p2.Process.Pid)}, *getEvent())
+
+	require.Error(t, p1.Wait())
+	assert.Equal(t, Event{Type: EventTypeProcessExit, Reason: EventReasonOOMKill, Pid: uint32(p1.Process.Pid)}, *getEvent())
+	assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: uint32(p2.Process.Pid)}, *getEvent())
+
+	var limit int64 = 200 * 1024 * 1024
+	control, err := cgroups.New(cgroups.V1, cgroups.StaticPath("/program"), &specs.LinuxResources{
+		Memory: &specs.LinuxMemory{Limit: &limit},
+	})
+	require.NoError(t, err)
+	defer control.Delete()
+	// p3 should be killed by the OOM killer, because 300 MB > 200 MB cgroup limit
+	p3 := exec.Command("cgexec", "-g", "memory:program", program, "300", "1s")
+	require.Error(t, p3.Run())
+	assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: uint32(p3.Process.Pid)}, *getEvent())
+	assert.Equal(t, Event{Type: EventTypeProcessExit, Reason: EventReasonOOMKill, Pid: uint32(p3.Process.Pid)}, *getEvent())
+
+	for {
+		e := getEvent()
+		if e == nil {
+			break
+		}
+		t.Errorf("unexpected event %+v", e)
+	}
+}
+
+func TestTcpEvents(t *testing.T) {
+	skipIfNotRoot(t)
+	l, err := net.Listen("tcp", "127.0.0.1:8080")
+	require.NoError(t, err)
+	listenAddr := l.Addr().String()
+	remoteAddr := "127.0.0.1:8080"
+	c, err := net.DialTimeout("tcp", remoteAddr, 100*time.Millisecond)
+	require.NoError(t, err)
+	localAddr := c.LocalAddr().String()
+	time.Sleep(100 * time.Millisecond)
+
+	getEvent, stop := runTracer(t, false)
+	defer stop()
+
+	pid := uint32(os.Getpid())
+
+	is := func(e *Event, typ EventType, sAddr string, dAddr string, pid uint32) bool {
+		if e == nil {
+			return false
+		}
+		sa := e.SrcAddr.String()
+		if strings.HasSuffix(sAddr, ":") {
+			sa = fmt.Sprintf("%s:", e.SrcAddr.IP())
+		}
+		da := e.DstAddr.String()
+		return e.Type == typ && e.Pid == pid && sa == sAddr && da == dAddr
+	}
+
+	listenFound := false
+	connectFound := false
+	for {
+		e := getEvent()
+		if e == nil {
+			break
+		}
+		if is(e, EventTypeListenOpen, listenAddr, "0.0.0.0:0", pid) {
+			listenFound = true
+		}
+		if is(e, EventTypeConnectionOpen, localAddr, remoteAddr, pid) {
+			connectFound = true
+		}
+	}
+	if !listenFound {
+		t.Errorf("expected %s on %s", EventTypeListenOpen, l.Addr())
+	}
+	if !connectFound {
+		t.Errorf("expected %s to %s", EventTypeConnectionOpen, l.Addr())
+	}
+
+	nextIs := func(typ EventType, sAddr string, dAddr string, pid uint32) {
+		e := getEvent()
+		if !is(e, typ, sAddr, dAddr, pid) {
+			expected := fmt.Sprintf("%-20s %6d: %s -> %s", typ, pid, sAddr, dAddr)
+			actual := "nil"
+			if e != nil {
+				actual = fmt.Sprintf("%-20s %6d: %s -> %s", e.Type, e.Pid, e.SrcAddr, e.DstAddr)
+			}
+			assert.Equal(t, expected, actual)
+		}
+	}
+
+	require.NoError(t, c.Close())
+	nextIs(EventTypeConnectionClose, localAddr, listenAddr, 0)
+	nextIs(EventTypeConnectionClose, listenAddr, localAddr, 0)
+
+	require.NoError(t, l.Close())
+	nextIs(EventTypeListenClose, listenAddr, "0.0.0.0:0", pid)
+
+	c, err = net.DialTimeout("tcp", listenAddr, 100*time.Millisecond)
+	require.Error(t, err)
+	nextIs(EventTypeConnectionError, "127.0.0.1:", listenAddr, pid)
+
+	l, err = net.Listen("tcp4", ":8080")
+	require.NoError(t, err)
+	listenAddr = l.Addr().String()
+	nextIs(EventTypeListenOpen, listenAddr, "0.0.0.0:0", pid)
+
+	c, err = net.DialTimeout("tcp", remoteAddr, 100*time.Millisecond)
+	require.NoError(t, err)
+	localAddr = c.LocalAddr().String()
+	nextIs(EventTypeConnectionOpen, localAddr, remoteAddr, pid)
+
+	require.NoError(t, exec.Command("tc", "qdisc", "add", "dev", "lo", "root", "netem", "loss", "100%").Run())
+	getEvent()
+	getEvent()
+	c.Write([]byte("hello"))
+	nextIs(EventTypeTCPRetransmit, localAddr, remoteAddr, 0)
+	require.NoError(t, exec.Command("tc", "qdisc", "del", "dev", "lo", "root", "netem").Run())
+	getEvent()
+	getEvent()
+	func() {
+		timer := time.NewTimer(time.Second)
+		for {
+			select {
+			case <-timer.C:
+				return
+			default:
+				e := getEvent()
+				require.True(t, e == nil || e.Type == EventTypeTCPRetransmit)
+			}
+		}
+	}()
+
+	require.NoError(t, c.Close())
+	nextIs(EventTypeConnectionClose, localAddr, remoteAddr, 0)
+	nextIs(EventTypeConnectionClose, remoteAddr, localAddr, 0)
+
+	require.NoError(t, l.Close())
+	nextIs(EventTypeListenClose, listenAddr, "0.0.0.0:0", pid)
+
+	for {
+		e := getEvent()
+		if e == nil {
+			break
+		}
+		t.Errorf("unexpected event %+v", e)
+	}
+}
+
+func TestFileEvents(t *testing.T) {
+	skipIfNotRoot(t)
+	src := `
+		package main
+		
+		import (
+			"os"
+			"strconv"
+			"syscall"
+			"unsafe"
+			"time"
+		)
+		
+		func main() {
+			call, _ := strconv.Atoi(os.Args[1])
+			path := os.Args[2]
+			flags, _ := strconv.Atoi(os.Args[3])
+			filename, _ := syscall.BytePtrFromString(path)
+			var err syscall.Errno
+			switch call {
+			case syscall.SYS_OPEN:
+				_, _, err = syscall.Syscall6(syscall.SYS_OPEN, uintptr(unsafe.Pointer(filename)), uintptr(flags), 0, 0, 0, 0)
+			case syscall.SYS_OPENAT:
+				AT_FDCWD := -100
+				_, _, err = syscall.Syscall6(syscall.SYS_OPENAT, uintptr(AT_FDCWD), uintptr(unsafe.Pointer(filename)), uintptr(flags), 0, 0, 0)
+			}
+			time.Sleep(100 * time.Millisecond)
+			os.Exit(int(err))
+		}
+	`
+	require.NoError(t, os.Chdir(t.TempDir()))
+	require.NoError(t, os.WriteFile("program.go", []byte(src), 0644))
+	out, err := exec.Command("go", "build", "-o", "program", "program.go").CombinedOutput()
+	require.Equal(t, "", string(out))
+	require.NoError(t, err)
+
+	getEvent, stop := runTracer(t, false)
+	defer stop()
+	for {
+		if e := getEvent(); e == nil {
+			break
+		}
+	}
+
+	for _, call := range []int{syscall.SYS_OPEN, syscall.SYS_OPENAT} {
+		run := func(file string, flag int) (uint32, error) {
+			p := exec.Command("./program", strconv.Itoa(call), file, strconv.Itoa(flag))
+			err := p.Run()
+			return uint32(p.Process.Pid), err
+		}
+
+		pid, err := run("program.go", os.O_RDONLY)
+		assert.NoError(t, err)
+		assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
+		assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
+
+		pid, err = run("program.go", os.O_WRONLY)
+		assert.NoError(t, err)
+		assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
+		assert.Equal(t, Event{Type: EventTypeFileOpen, Pid: pid, Fd: 3}, *getEvent())
+		assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
+
+		pid, err = run("program.go", os.O_RDWR)
+		assert.NoError(t, err)
+		assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
+		assert.Equal(t, Event{Type: EventTypeFileOpen, Pid: pid, Fd: 3}, *getEvent())
+		assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
+
+		// open error: text file busy
+		pid, err = run("program", os.O_RDWR)
+		assert.Error(t, err)
+		assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
+		assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
+
+		// ignoring /proc/*, /dev/*, /sys/*
+		for _, f := range []string{"/proc/sys/fs/file-max", "/dev/null", "/sys/kernel/profiling"} {
+			pid, err = run(f, os.O_RDWR)
+			assert.NoError(t, err)
+			assert.Equal(t, Event{Type: EventTypeProcessStart, Pid: pid}, *getEvent())
+			assert.Equal(t, Event{Type: EventTypeProcessExit, Pid: pid}, *getEvent())
+		}
+
+		for {
+			e := getEvent()
+			if e == nil {
+				break
+			}
+			t.Errorf("unexpected event %+v", e)
+		}
+	}
+}
+
+func runTracer(t *testing.T, verbose bool) (func() *Event, func()) {
+	events := make(chan Event, 1000)
+	done := make(chan bool, 1)
+
+	var uname unix.Utsname
+	assert.NoError(t, unix.Uname(&uname))
+
+	go func() {
+		tt, err := NewTracer(events, string(bytes.Split(uname.Release[:], []byte{0})[0]))
+		require.NoError(t, err)
+		<-done
+		tt.Close()
+	}()
+
+	stop := func() {
+		done <- true
+	}
+
+	get := func() *Event {
+		select {
+		case e := <-events:
+			if verbose {
+				fmt.Printf("%+v\n", e)
+			}
+			return &e
+		case <-time.NewTimer(time.Second).C:
+			return nil
+		}
+	}
+
+	return get, stop
+}

+ 46 - 0
flags/flags.go

@@ -0,0 +1,46 @@
+package flags
+
+import (
+	"gopkg.in/alecthomas/kingpin.v2"
+	"inet.af/netaddr"
+	"k8s.io/klog/v2"
+	"os"
+	"strings"
+)
+
+var (
+	ListenAddress             = kingpin.Flag("listen", "Listen address - ip:port or :port").Default("0.0.0.0:80").String()
+	CgroupRoot                = kingpin.Flag("cgroupfs-root", "The mount point of the host cgroupfs root").Default("/sys/fs/cgroup").String()
+	NoParseLogs               = kingpin.Flag("no-parse-logs", "Disable container logs parsing").Default("false").Bool()
+	NoPingUpstreams           = kingpin.Flag("no-ping-upstreams", "Disable container upstreams ping").Default("false").Bool()
+	externalNetworksWhitelist = kingpin.Flag("track-public-network", "Allow track connections to the specified IP networks, all private networks are allowed by default (e.g., Y.Y.Y.Y/mask)").Strings()
+	ExternalNetworksWhitelist []netaddr.IPPrefix
+
+	Provider         = kingpin.Flag("provider", "`provider` label for `node_cloud_info` metric").String()
+	Region           = kingpin.Flag("region", "`region` label for `node_cloud_info` metric").String()
+	AvailabilityZone = kingpin.Flag("availability-zone", "`availability_zone` label for `node_cloud_info` metric").String()
+)
+
+func GetString(fl *string) string {
+	if fl == nil {
+		return ""
+	}
+	return *fl
+}
+
+func init() {
+	if strings.HasSuffix(os.Args[0], ".test") {
+		return
+	}
+	kingpin.HelpFlag.Short('h').Hidden()
+	kingpin.Parse()
+	if externalNetworksWhitelist != nil {
+		for _, prefix := range *externalNetworksWhitelist {
+			p, err := netaddr.ParseIPPrefix(prefix)
+			if err != nil {
+				klog.Fatalf("invalid network %s: %s", prefix, err)
+			}
+			ExternalNetworksWhitelist = append(ExternalNetworksWhitelist, p)
+		}
+	}
+}

+ 37 - 0
go.mod

@@ -0,0 +1,37 @@
+module github.com/coroot/coroot-node-agent
+
+go 1.16
+
+require (
+	cloud.google.com/go v0.54.0
+	github.com/Microsoft/go-winio v0.4.17 // indirect
+	github.com/Microsoft/hcsshim v0.8.16 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.8.1
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0
+	github.com/cilium/ebpf v0.6.2
+	github.com/containerd/cgroups v1.0.1
+	github.com/containerd/containerd v1.5.0-rc.0
+	github.com/containerd/continuity v0.1.0 // indirect
+	github.com/containerd/fifo v1.0.0 // indirect
+	github.com/containerd/typeurl v1.0.2 // indirect
+	github.com/coreos/go-systemd/v22 v22.3.2
+	github.com/coroot/logparser v1.0.0
+	github.com/docker/docker v20.10.8+incompatible
+	github.com/docker/go-connections v0.4.0 // indirect
+	github.com/florianl/go-conntrack v0.3.0
+	github.com/google/uuid v1.2.0 // indirect
+	github.com/mdlayher/taskstats v0.0.0-20210730152605-7c2d9360c326
+	github.com/morikuni/aec v1.0.0 // indirect
+	github.com/opencontainers/runc v1.0.1 // indirect
+	github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
+	github.com/prometheus/client_golang v1.11.0
+	github.com/stretchr/testify v1.7.0
+	github.com/vishvananda/netlink v1.1.0
+	github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f
+	golang.org/x/mod v0.3.0
+	golang.org/x/net v0.0.0-20210913180222-943fd674d43e
+	golang.org/x/sys v0.0.0-20210915083310-ed5796bab164
+	gopkg.in/alecthomas/kingpin.v2 v2.2.6
+	inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e
+	k8s.io/klog/v2 v2.20.0
+)

+ 1060 - 0
go.sum

@@ -0,0 +1,1060 @@
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
+github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
+github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
+github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
+github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
+github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w=
+github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
+github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
+github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
+github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
+github.com/Microsoft/hcsshim v0.8.16 h1:8/auA4LFIZFTGrqfKhGBSXwM6/4X1fHa/xniyEHu8ac=
+github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
+github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
+github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
+github.com/aws/aws-sdk-go-v2 v1.9.0 h1:+S+dSqQCN3MSU5vJRu1HqHrq00cJn6heIMU7X9hcsoo=
+github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
+github.com/aws/aws-sdk-go-v2/config v1.8.1 h1:AcAenV2NVwOViG+3ts73uT08L1olN4NBNNz7lUlHSUo=
+github.com/aws/aws-sdk-go-v2/config v1.8.1/go.mod h1:AQtpYfVYjuuft4Dgh0jGSkPQJ9MvmK9vXfSub7oSXlI=
+github.com/aws/aws-sdk-go-v2/credentials v1.4.1 h1:oDiUP50hKRwC6xAgESAj46lgL2prJRZQWnCBzn+TU/c=
+github.com/aws/aws-sdk-go-v2/credentials v1.4.1/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0 h1:OxTAgH8Y4BXHD6PGCJ8DHx2kaZPCQfSTqmDsdRZFezE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0/go.mod h1:CpNzHK9VEFUCknu50kkB8z58AH2B5DvPP7ea1LHve/Y=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.2.2 h1:d95cddM3yTm4qffj3P6EnP+TzX1SSkWaQypXSgT/hpA=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.2.2/go.mod h1:BQV0agm+JEhqR+2RT5e1XTFIDcAAV0eW6z2trp+iduw=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.0 h1:VNJ5NLBteVXEwE2F1zEXVmyIH58mZ6kIQGJoC7C+vkg=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.0/go.mod h1:R1KK+vY8AfalhG1AOu5e35pOD2SdoPKQCFLTvnxiohk=
+github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 h1:sHXMIKYS6YiLPzmKSvDpPmOpJDHxmAUgbiF49YNVztg=
+github.com/aws/aws-sdk-go-v2/service/sso v1.4.0/go.mod h1:+1fpWnL96DL23aXPpMGbsmKe8jLTEfbjuQoA4WS1VaA=
+github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 h1:1at4e5P+lvHNl2nUktdM2/v+rpICg/QSEr9TO/uW9vU=
+github.com/aws/aws-sdk-go-v2/service/sts v1.7.0/go.mod h1:0qcSMCyASQPN2sk/1KQLQ2Fh6yq8wm0HSDAimPhzCoM=
+github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc=
+github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
+github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
+github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
+github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
+github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
+github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
+github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
+github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
+github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
+github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/cilium/ebpf v0.6.2 h1:iHsfF/t4aW4heW2YKfeHrVPGdtYTL4C4KocpM8KTSnI=
+github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
+github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
+github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
+github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E=
+github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
+github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=
+github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
+github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
+github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
+github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ=
+github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=
+github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
+github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
+github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=
+github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=
+github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
+github.com/containerd/containerd v1.5.0-rc.0 h1:fVmAxX648SbHlWm3UnrkKQrZ+aeXznUnZttjbIYCF60=
+github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
+github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
+github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
+github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
+github.com/containerd/continuity v0.1.0 h1:UFRRY5JemiAhPZrr/uE0n8fMTLcZsUvySPr1+D7pgr8=
+github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
+github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/fifo v1.0.0 h1:6PirWBr9/L7GDamKr+XM0IeUFXu5mf3M/BPpH9gaLBU=
+github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU=
+github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
+github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
+github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0=
+github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA=
+github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow=
+github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=
+github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
+github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=
+github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/ttrpc v1.0.2 h1:2/O3oTZN36q2xRolk0a2WWGgh7/Vf/liElg5hFYLX9U=
+github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
+github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=
+github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
+github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY=
+github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=
+github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw=
+github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y=
+github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=
+github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=
+github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
+github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coroot/logparser v1.0.0 h1:1uB9cMmqHsj7rU4QDtR6IGmMCVdWzX2NlQQDc6Mf6S4=
+github.com/coroot/logparser v1.0.0/go.mod h1:GHsVO1xE8pR5mmu9Eiop9IXHwN/QzNRx1s0fuzVxq7I=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
+github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
+github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
+github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
+github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
+github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
+github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM=
+github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/florianl/go-conntrack v0.3.0 h1:DUY84Mce+/lE9dJi2EWvGYacQtX2X96J9aVWV99l8UE=
+github.com/florianl/go-conntrack v0.3.0/go.mod h1:Q+Um4J/nWUXSbnyzQRMOP4eweSeEQ2G8sfCO5gMz6Pw=
+github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
+github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v1.0.0 h1:kH951GinvFVaQgy/ki/B3YYmQtRpExGigSJg6O8z5jo=
+github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
+github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
+github.com/gogo/googleapis v1.4.0 h1:zgVt4UpGxcqVOw97aRGxT4svlcmdK35fynLNctY32zI=
+github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
+github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
+github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
+github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
+github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
+github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
+github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
+github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
+github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
+github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
+github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
+github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190 h1:iycCSDo8EKVueI9sfVBBJmtNn9DnXV/K1YWwEJO+uOs=
+github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
+github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
+github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
+github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
+github.com/mdlayher/genetlink v0.0.0-20190313224034-60417448a851/go.mod h1:EsbsAEUEs15qC1cosAwxgCWV0Qhd8TmkxnA9Kw1Vhl4=
+github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
+github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
+github.com/mdlayher/netlink v0.0.0-20190313131330-258ea9dff42c/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
+github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
+github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
+github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
+github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
+github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
+github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
+github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
+github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
+github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
+github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI=
+github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
+github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 h1:qEtkL8n1DAHpi5/AOgAckwGQUlMe4+jhL/GMt+GKIks=
+github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc=
+github.com/mdlayher/taskstats v0.0.0-20210730152605-7c2d9360c326 h1:KAmPknckS0G1U7IS6KAZDPwku0Y6/aED7kFbJ17uPVc=
+github.com/mdlayher/taskstats v0.0.0-20210730152605-7c2d9360c326/go.mod h1:4BgwXfsNpEQuhyL29eN7QCDsmKFOI69p2DmC4Y5vF40=
+github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
+github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM=
+github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
+github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI=
+github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
+github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=
+github.com/opencontainers/runc v1.0.1 h1:G18PGckGdAm3yVQRWDVQ1rLSLntiniKJ0cNRT2Tm5gs=
+github.com/opencontainers/runc v1.0.1/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
+github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
+github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
+github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
+github.com/opencontainers/selinux v1.8.2 h1:c4ca10UMgRcvZ6h0K4HtS15UaVSBEaE+iln2LVpAuGc=
+github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
+github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
+github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
+github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
+github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
+go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
+go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
+go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
+golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8=
+golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 h1:7ZDGnxgHAMw7thfC5bEos0RDAccZKxioiWBhfIe+tvw=
+golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8=
+google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
+gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e h1:tvgqez5ZQoBBiBAGNU/fmJy247yB/7++kcLOEoMYup0=
+inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
+k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
+k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
+k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
+k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
+k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
+k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
+k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
+k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
+k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
+k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/klog/v2 v2.20.0 h1:tlyxlSvd63k7axjhuchckaRJm+a92z5GSOrTOQY5sHw=
+k8s.io/klog/v2 v2.20.0/go.mod h1:Gm8eSIfQN6457haJuPaMxZw4wyP5k+ykPFlrhQDvhvw=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
+k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

+ 107 - 0
logs/journald_reader.go

@@ -0,0 +1,107 @@
+package logs
+
+import (
+	"fmt"
+	"github.com/coreos/go-systemd/v22/sdjournal"
+	"github.com/coroot/logparser"
+	"k8s.io/klog/v2"
+	"strings"
+	"sync"
+	"time"
+)
+
+type JournaldReader struct {
+	journal     *sdjournal.Journal
+	subscribers map[string]chan<- logparser.LogEntry
+	until       chan time.Time
+	lock        sync.Mutex
+}
+
+func NewJournaldReader(journalPaths ...string) (*JournaldReader, error) {
+	r := &JournaldReader{
+		until:       make(chan time.Time),
+		subscribers: map[string]chan<- logparser.LogEntry{},
+	}
+	var err error
+	for _, journalPath := range journalPaths {
+		if r.journal, err = sdjournal.NewJournalFromDir(journalPath); err != nil {
+			continue
+		}
+		usage, err := r.journal.GetUsage()
+		if err != nil {
+			continue
+		}
+		if usage == 0 {
+			r.journal = nil
+			continue
+		}
+		if err = r.journal.SeekRealtimeUsec(uint64(time.Now().Add(time.Millisecond).UnixNano() / 1000)); err != nil {
+			return nil, err
+		}
+		//klog.Infof("systemd journal found in %s", journalPath)
+		break
+	}
+	if r.journal == nil {
+		return nil, fmt.Errorf("systemd journal not found in %s", strings.Join(journalPaths, ","))
+	}
+	go r.follow()
+	return r, nil
+}
+
+func (r *JournaldReader) follow() {
+	for {
+		c, err := r.journal.Next()
+		if err != nil {
+			klog.Errorln("failed to read journal:", err)
+			return
+		}
+		if c <= 0 {
+			r.journal.Wait(time.Millisecond * 100)
+			continue
+		}
+		e, err := r.journal.GetEntry()
+		if err != nil {
+			klog.Errorf("failed to read entry from journal")
+			return
+		}
+		msg := e.Fields[sdjournal.SD_JOURNAL_FIELD_MESSAGE]
+		if msg == "" {
+			continue
+		}
+		le := logparser.LogEntry{
+			Content: msg,
+			Level:   logparser.LevelByPriority(e.Fields[sdjournal.SD_JOURNAL_FIELD_PRIORITY]),
+		}
+		r.lock.Lock()
+		ch, ok := r.subscribers[e.Fields[sdjournal.SD_JOURNAL_FIELD_SYSTEMD_CGROUP]]
+		r.lock.Unlock()
+		if !ok {
+			continue
+		}
+		ch <- le
+	}
+}
+
+func (r *JournaldReader) Subscribe(cgroup string, ch chan<- logparser.LogEntry) error {
+	r.lock.Lock()
+	defer r.lock.Unlock()
+	if _, ok := r.subscribers[cgroup]; ok {
+		return fmt.Errorf(`duplicate subscriber for cgroup %s`, cgroup)
+	}
+	r.subscribers[cgroup] = ch
+	return nil
+}
+
+func (r *JournaldReader) Unsubscribe(cgroup string) {
+	r.lock.Lock()
+	defer r.lock.Unlock()
+	if _, ok := r.subscribers[cgroup]; !ok {
+		klog.Warning("unknown subscriber for cgroup", cgroup)
+		return
+	}
+	delete(r.subscribers, cgroup)
+}
+
+func (r *JournaldReader) Close() {
+	_ = r.journal.Close()
+}

+ 153 - 0
logs/tail_reader.go

@@ -0,0 +1,153 @@
+package logs
+
+import (
+	"bufio"
+	"context"
+	"github.com/coroot/logparser"
+	"io"
+	"k8s.io/klog/v2"
+	"os"
+	"strings"
+	"time"
+)
+
+var (
+	tailPollInterval = time.Second
+)
+
+type TailReader struct {
+	fileName string
+	ch       chan<- logparser.LogEntry
+
+	file   *os.File
+	info   os.FileInfo
+	reader *bufio.Reader
+
+	stop    context.CancelFunc
+	stopped chan struct{}
+}
+
+func NewTailReader(fileName string, ch chan<- logparser.LogEntry) (*TailReader, error) {
+	ctx, cancel := context.WithCancel(context.Background())
+	r := &TailReader{
+		fileName: fileName,
+		ch:       ch,
+		stop:     cancel,
+		stopped:  make(chan struct{}),
+	}
+	var err error
+	if r.file, err = os.Open(fileName); err != nil {
+		return nil, err
+	}
+	if r.info, err = r.file.Stat(); err != nil {
+		return nil, err
+	}
+	if _, err = r.file.Seek(0, io.SeekEnd); err != nil {
+		return nil, err
+	}
+	r.reader = bufio.NewReader(r.file)
+
+	go func() {
+		var prefix string
+		for {
+			select {
+			case <-ctx.Done():
+				r.stopped <- struct{}{}
+				return
+			default:
+				line, err := r.reader.ReadString('\n')
+				if err != nil {
+					prefix = line
+					r.poll(ctx)
+					continue
+				}
+				if prefix != "" {
+					line = prefix + line
+					prefix = ""
+				}
+				r.ch <- logparser.LogEntry{Content: strings.TrimSuffix(line, "\n"), Level: logparser.LevelUnknown}
+			}
+		}
+	}()
+
+	return r, nil
+}
+
+func (r *TailReader) Stop() {
+	klog.Infoln("stopping tail reader for", r.fileName)
+	r.stop()
+	<-r.stopped
+	if r.file != nil {
+		_ = r.file.Close()
+	}
+}
+
+func (r *TailReader) poll(ctx context.Context) {
+	ticker := time.NewTicker(tailPollInterval)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-ticker.C:
+			if info, err := os.Stat(r.fileName); err != nil {
+				if r.file != nil {
+					_ = r.file.Close()
+					r.file = nil
+				}
+			} else {
+				if r.file == nil {
+					f, err := os.Open(r.fileName)
+					if err != nil {
+						continue
+					}
+					r.file = f
+					r.info = info
+					r.reader = bufio.NewReader(r.file)
+					return
+				}
+				if r.moved(info) || r.truncated(info) || r.appended(info) {
+					r.info = info
+					return
+				}
+			}
+		}
+	}
+}
+
+func (r *TailReader) moved(info os.FileInfo) bool {
+	if !os.SameFile(r.info, info) {
+		f, err := os.Open(r.fileName)
+		if err != nil {
+			r.file = nil
+			return false
+		}
+		_ = r.file.Close()
+		r.file = f
+		r.reader = bufio.NewReader(r.file)
+		return true
+	}
+	return false
+}
+
+func (r *TailReader) truncated(info os.FileInfo) bool {
+	if r.file == nil {
+		return false
+	}
+	if info.Size() < r.info.Size() {
+		if _, err := r.file.Seek(0, io.SeekStart); err == nil {
+			return true
+		}
+	}
+	return false
+}
+
+func (r *TailReader) appended(info os.FileInfo) bool {
+	if r.file == nil {
+		return false
+	}
+	if info.Size() > r.info.Size() {
+		return true
+	}
+	return false
+}

+ 76 - 0
logs/tail_reader_test.go

@@ -0,0 +1,76 @@
+package logs
+
+import (
+	"github.com/coroot/logparser"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"os"
+	"testing"
+	"time"
+)
+
+func TestTailReader(t *testing.T) {
+	f, err := ioutil.TempFile("/tmp", "log")
+	assert.NoError(t, err)
+	defer os.Remove(f.Name())
+
+	tailPollInterval = time.Millisecond * 100
+	ch := make(chan logparser.LogEntry, 10)
+	tr, err := NewTailReader(f.Name(), ch)
+	assert.NoError(t, err)
+	defer tr.Stop()
+
+	write := func(s string) {
+		_, err = f.WriteString(s)
+		assert.NoError(t, err)
+	}
+
+	wait := func() {
+		time.Sleep(3 * tailPollInterval)
+	}
+
+	get := func(expected string) {
+		entry := <-ch
+		assert.Equal(t, expected, entry.Content)
+	}
+
+	write("foo 1\n")
+	get("foo 1")
+
+	// append
+	write("bar 1\nbuz 1\n")
+	get("bar 1")
+	get("buz 1")
+
+	// no end of line
+	write("foo 2\nba")
+	wait()
+	write("r 2\n")
+	get("foo 2")
+	get("bar 2")
+
+	// move
+	err = os.Rename(f.Name(), f.Name()+".1")
+	assert.NoError(t, err)
+	defer os.Remove(f.Name() + ".1")
+	f, err = os.Create(f.Name())
+	assert.NoError(t, err)
+	write("foo 3\nbar 3\n")
+	get("foo 3")
+	get("bar 3")
+
+	// truncate
+	f, err = os.OpenFile(f.Name(), os.O_WRONLY|os.O_TRUNC, 0)
+	assert.NoError(t, err)
+	write("foo 4\n")
+	get("foo 4")
+
+	// delete
+	err = os.Remove(f.Name())
+	assert.NoError(t, err)
+	wait()
+	f, err = os.Create(f.Name())
+	assert.NoError(t, err)
+	write("foo 5\n")
+	get("foo 5")
+}

+ 125 - 0
main.go

@@ -0,0 +1,125 @@
+package main
+
+import (
+	"bytes"
+	"github.com/coroot/coroot-node-agent/common"
+	"github.com/coroot/coroot-node-agent/containers"
+	"github.com/coroot/coroot-node-agent/flags"
+	"github.com/coroot/coroot-node-agent/node"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"golang.org/x/mod/semver"
+	"golang.org/x/sys/unix"
+	"k8s.io/klog/v2"
+	"net/http"
+	_ "net/http/pprof"
+	"os"
+	"path"
+	"runtime"
+	"strings"
+)
+
+var (
+	version = "unknown"
+)
+
+const minSupportedKernelVersion = "4.16"
+
+func uname() (string, string, error) {
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+
+	f, err := os.Open("/proc/1/ns/uts")
+	if err != nil {
+		return "", "", err
+	}
+	defer f.Close()
+
+	self, err := os.Open("/proc/self/ns/uts")
+	if err != nil {
+		return "", "", err
+	}
+	defer self.Close()
+
+	defer func() {
+		unix.Setns(int(self.Fd()), unix.CLONE_NEWUTS)
+	}()
+
+	err = unix.Setns(int(f.Fd()), unix.CLONE_NEWUTS)
+	if err != nil {
+		return "", "", err
+	}
+	var utsname unix.Utsname
+	if err := unix.Uname(&utsname); err != nil {
+		return "", "", err
+	}
+	hostname := string(bytes.Split(utsname.Nodename[:], []byte{0})[0])
+	kernelVersion := string(bytes.Split(utsname.Release[:], []byte{0})[0])
+	return hostname, kernelVersion, nil
+}
+
+func machineID() string {
+	for _, p := range []string{"sys/devices/virtual/dmi/id/product_uuid", "etc/machine-id", "var/lib/dbus/machine-id"} {
+		payload, err := os.ReadFile(path.Join("/proc/1/root", p))
+		if err != nil {
+			klog.Warningln("failed to read machine-id:", err)
+			continue
+		}
+		id := strings.TrimSpace(strings.Replace(string(payload), "-", "", -1))
+		klog.Infoln("machine-id: ", id)
+		return id
+	}
+	return ""
+}
+
+func main() {
+	klog.Infoln("agent version:", version)
+
+	hostname, kv, err := uname()
+	if err != nil {
+		klog.Exitln("failed to get uname:", err)
+	}
+	klog.Infoln("hostname:", hostname)
+	klog.Infoln("kernel version:", kv)
+
+	ver := common.KernelMajorMinor(kv)
+	if ver == "" {
+		klog.Exitln("invalid kernel version:", kv)
+	}
+	if semver.Compare("v"+ver, "v"+minSupportedKernelVersion) == -1 {
+		klog.Exitf("the minimum Linux kernel version required is %s or later", minSupportedKernelVersion)
+	}
+	registry := prometheus.NewRegistry()
+	registerer := prometheus.WrapRegistererWith(prometheus.Labels{"machine_id": machineID()}, registry)
+
+	registerer.MustRegister(info("node_agent_info", version))
+
+	if err := registerer.Register(node.NewCollector(hostname, kv)); err != nil {
+		klog.Exitln(err)
+	}
+
+	cs, err := containers.NewRegistry(registerer, kv)
+	if err != nil {
+		klog.Exitln(err)
+	}
+	defer cs.Close()
+
+	http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorLog: logger{}, Registry: registerer}))
+	klog.Infoln("listening on:", *flags.ListenAddress)
+	klog.Errorln(http.ListenAndServe(*flags.ListenAddress, nil))
+}
+
+func info(name, version string) prometheus.Collector {
+	g := prometheus.NewGauge(prometheus.GaugeOpts{
+		Name:        name,
+		ConstLabels: prometheus.Labels{"version": version},
+	})
+	g.Set(1)
+	return g
+}
+
+type logger struct{}
+
+func (l logger) Println(v ...interface{}) {
+	klog.Errorln(v...)
+}

+ 52 - 0
manifests/coroot-node-agent.yaml

@@ -0,0 +1,52 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: coroot
+
+---
+
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  labels:
+    app: coroot-node-agent
+  name: coroot-node-agent
+  namespace: coroot
+spec:
+  selector:
+    matchLabels:
+      app: coroot-node-agent
+  template:
+    metadata:
+      labels:
+        app: coroot-node-agent
+      annotations:
+        prometheus.io/scrape: 'true'
+        prometheus.io/port: '80'
+    spec:
+      tolerations:
+        - operator: Exists
+      hostPID: true
+      containers:
+        - name: coroot-node-agent
+          image: ghcr.io/coroot/coroot-node-agent
+          args: ["--cgroupfs-root", "/host/sys/fs/cgroup"]
+          ports:
+            - containerPort: 80
+              name: http
+          securityContext:
+            privileged: true
+          volumeMounts:
+            - mountPath: /host/sys/fs/cgroup
+              name: cgroupfs
+              readOnly: true
+            - mountPath: /sys/kernel/debug
+              name: debugfs
+              readOnly: false
+      volumes:
+        - hostPath:
+            path: /sys/fs/cgroup
+          name: cgroupfs
+        - hostPath:
+            path: /sys/kernel/debug
+          name: debugfs

+ 266 - 0
node/collector.go

@@ -0,0 +1,266 @@
+package node
+
+import (
+	"github.com/coroot/coroot-node-agent/common"
+	"github.com/coroot/coroot-node-agent/flags"
+	"github.com/coroot/coroot-node-agent/node/metadata"
+	"github.com/prometheus/client_golang/prometheus"
+	"k8s.io/klog/v2"
+)
+
+var (
+	procRoot = "/proc"
+
+	infoDesc = prometheus.NewDesc(
+		"node_info",
+		"Meta information about the node",
+		[]string{"hostname", "kernel_version"}, nil,
+	)
+	cloudInfoDesc = prometheus.NewDesc(
+		"node_cloud_info",
+		"Meta information about the cloud instance",
+		[]string{"provider", "account_id", "instance_id", "instance_type", "instance_life_cycle", "region", "availability_zone", "availability_zone_id", "local_ipv4", "public_ipv4"}, nil,
+	)
+	cpuUsageDesc = prometheus.NewDesc(
+		"node_resources_cpu_usage_seconds_total",
+		"The amount of CPU time spent in each mode",
+		[]string{"mode"}, nil,
+	)
+	cpuLogicalCoresDesc = prometheus.NewDesc(
+		"node_resources_cpu_logical_cores",
+		"The number of logical CPU cores",
+		nil, nil,
+	)
+	memTotalDesc = prometheus.NewDesc(
+		"node_resources_memory_total_bytes",
+		"The total amount of physical memory",
+		nil, nil,
+	)
+	memFreeDesc = prometheus.NewDesc(
+		"node_resources_memory_free_bytes",
+		"The amount of unassigned memory",
+		nil, nil,
+	)
+	memAvailableDesc = prometheus.NewDesc(
+		"node_resources_memory_available_bytes",
+		"The total amount of available memory",
+		nil, nil,
+	)
+	memCacheDesc = prometheus.NewDesc(
+		"node_resources_memory_cached_bytes",
+		"The amount of memory used as page cache",
+		nil, nil,
+	)
+	diskReadsDesc = prometheus.NewDesc(
+		"node_resources_disk_reads_total",
+		"The total number of reads completed successfully",
+		[]string{"device"}, nil,
+	)
+	diskWritesDesc = prometheus.NewDesc(
+		"node_resources_disk_writes_total",
+		"The total number of writes completed successfully",
+		[]string{"device"}, nil,
+	)
+	diskReadBytesDesc = prometheus.NewDesc(
+		"node_resources_disk_read_bytes_total",
+		"The total number of bytes read from the disk",
+		[]string{"device"}, nil,
+	)
+	diskWrittenBytesDesc = prometheus.NewDesc(
+		"node_resources_disk_written_bytes_total",
+		"The total number of bytes written to the disk",
+		[]string{"device"}, nil,
+	)
+	diskReadTimeDesc = prometheus.NewDesc(
+		"node_resources_disk_read_time_seconds_total",
+		"The total number of seconds spent reading",
+		[]string{"device"}, nil,
+	)
+	diskWriteTimeDesc = prometheus.NewDesc(
+		"node_resources_disk_write_time_seconds_total",
+		"The total number of seconds spent writing",
+		[]string{"device"}, nil,
+	)
+	diskIoTimeDesc = prometheus.NewDesc(
+		"node_resources_disk_io_time_seconds_total",
+		"The total number of seconds the disk spent doing I/O",
+		[]string{"device"}, nil,
+	)
+	netRxBytesDesc = prometheus.NewDesc(
+		"node_net_received_bytes_total",
+		"The total number of bytes received",
+		[]string{"interface"}, nil,
+	)
+	netTxBytesDesc = prometheus.NewDesc(
+		"node_net_transmitted_bytes_total",
+		"The total number of bytes transmitted",
+		[]string{"interface"}, nil,
+	)
+	netRxPacketsDesc = prometheus.NewDesc(
+		"node_net_received_packets_total",
+		"The total number of packets received",
+		[]string{"interface"}, nil,
+	)
+	netTxPacketsDesc = prometheus.NewDesc(
+		"node_net_transmitted_packets_total",
+		"The total number of packets transmitted",
+		[]string{"interface"}, nil,
+	)
+	netIfaceUpDesc = prometheus.NewDesc(
+		"node_net_interface_up",
+		"Status of the interface (0:down, 1:up)",
+		[]string{"interface"}, nil,
+	)
+	ipDesc = prometheus.NewDesc(
+		"node_net_interface_ip",
+		"IP address assigned to the interface",
+		[]string{"interface", "ip"}, nil,
+	)
+)
+
+type MemoryStat struct {
+	TotalBytes     float64
+	FreeBytes      float64
+	AvailableBytes float64
+	CachedBytes    float64
+}
+
+type CpuStat struct {
+	TotalUsage   CpuUsage
+	LogicalCores int
+}
+
+type CpuUsage struct {
+	User    float64
+	Nice    float64
+	System  float64
+	Idle    float64
+	IoWait  float64
+	Irq     float64
+	SoftIrq float64
+	Steal   float64
+}
+
+type Collector struct {
+	hostname         string
+	kernelVersion    string
+	instanceMetadata *metadata.CloudMetadata
+}
+
+func NewCollector(hostname, kernelVersion string) *Collector {
+	md := metadata.GetInstanceMetadata()
+	klog.Infof("instance metadata: %+v", md)
+	return &Collector{
+		hostname:         hostname,
+		kernelVersion:    kernelVersion,
+		instanceMetadata: md,
+	}
+}
+
+func (c *Collector) Collect(ch chan<- prometheus.Metric) {
+	ch <- gauge(infoDesc, 1, c.hostname, c.kernelVersion)
+	cpu, err := cpuStat(procRoot)
+	if err != nil {
+		if !common.IsNotExist(err) {
+			klog.Errorln(err)
+		}
+	} else {
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.User, "user")
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.Nice, "nice")
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.System, "system")
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.Idle, "idle")
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.IoWait, "iowait")
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.Irq, "irq")
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.SoftIrq, "softirq")
+		ch <- counter(cpuUsageDesc, cpu.TotalUsage.Steal, "steal")
+		ch <- gauge(cpuLogicalCoresDesc, float64(cpu.LogicalCores))
+	}
+
+	mem, err := memoryInfo(procRoot)
+	if err != nil {
+		if !common.IsNotExist(err) {
+			klog.Errorln(err)
+		}
+	} else {
+		ch <- gauge(memTotalDesc, mem.TotalBytes)
+		ch <- gauge(memFreeDesc, mem.FreeBytes)
+		ch <- gauge(memAvailableDesc, mem.AvailableBytes)
+		ch <- gauge(memCacheDesc, mem.CachedBytes)
+	}
+
+	disks, err := GetDisks()
+	if err != nil {
+		klog.Errorln("failed to get disk stats:", err)
+	} else {
+		for _, d := range disks.BlockDevices() {
+			ch <- counter(diskReadsDesc, d.ReadOps, d.Name)
+			ch <- counter(diskWritesDesc, d.WriteOps, d.Name)
+			ch <- counter(diskReadBytesDesc, d.BytesRead, d.Name)
+			ch <- counter(diskWrittenBytesDesc, d.BytesWritten, d.Name)
+			ch <- counter(diskReadTimeDesc, d.ReadTimeSeconds, d.Name)
+			ch <- counter(diskWriteTimeDesc, d.WriteTimeSeconds, d.Name)
+			ch <- counter(diskIoTimeDesc, d.IoTimeSeconds, d.Name)
+		}
+	}
+
+	netdev, err := netDevices()
+	if err != nil {
+		klog.Errorln(err)
+	} else {
+		for _, dev := range netdev {
+			ch <- counter(netRxBytesDesc, dev.RxBytes, dev.Name)
+			ch <- counter(netTxBytesDesc, dev.TxBytes, dev.Name)
+			ch <- counter(netRxPacketsDesc, dev.RxPackets, dev.Name)
+			ch <- counter(netTxPacketsDesc, dev.TxPackets, dev.Name)
+			ch <- gauge(netIfaceUpDesc, dev.Up, dev.Name)
+			for _, ip := range dev.Addresses {
+				ch <- gauge(ipDesc, 1, dev.Name, ip)
+			}
+		}
+	}
+
+	if c.instanceMetadata != nil {
+		im := c.instanceMetadata
+		ch <- gauge(cloudInfoDesc, 1,
+			string(im.Provider), im.AccountId, im.InstanceId, im.InstanceType, im.LifeCycle,
+			im.Region, im.AvailabilityZone, im.AvailabilityZoneId, im.LocalIPv4, im.PublicIPv4,
+		)
+	} else if flags.Provider != nil || flags.Region != nil || flags.AvailabilityZone != nil {
+		ch <- gauge(cloudInfoDesc, 1,
+			flags.GetString(flags.Provider), "", "", "", "",
+			flags.GetString(flags.Region), flags.GetString(flags.AvailabilityZone), "", "", "",
+		)
+	}
+}
+
+func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
+	ch <- infoDesc
+	ch <- cloudInfoDesc
+	ch <- cpuUsageDesc
+	ch <- cpuLogicalCoresDesc
+	ch <- memTotalDesc
+	ch <- memFreeDesc
+	ch <- memAvailableDesc
+	ch <- memCacheDesc
+	ch <- diskReadsDesc
+	ch <- diskWritesDesc
+	ch <- diskReadBytesDesc
+	ch <- diskWrittenBytesDesc
+	ch <- diskReadTimeDesc
+	ch <- diskWriteTimeDesc
+	ch <- diskIoTimeDesc
+	ch <- netRxBytesDesc
+	ch <- netTxBytesDesc
+	ch <- netRxPacketsDesc
+	ch <- netTxPacketsDesc
+	ch <- netIfaceUpDesc
+	ch <- ipDesc
+}
+
+func counter(desc *prometheus.Desc, value float64, labelValues ...string) prometheus.Metric {
+	return prometheus.MustNewConstMetric(desc, prometheus.CounterValue, value, labelValues...)
+}
+
+func gauge(desc *prometheus.Desc, value float64, labelValues ...string) prometheus.Metric {
+	return prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, value, labelValues...)
+}

+ 63 - 0
node/cpu.go

@@ -0,0 +1,63 @@
+package node
+
+import (
+	"io/ioutil"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+const CLOCKS_PER_SEC = float64(100)
+
+var (
+	cpuCorePrefix = regexp.MustCompile(`cpu\d+`)
+)
+
+func cpuStat(procRoot string) (CpuStat, error) {
+	stat := CpuStat{}
+	data, err := ioutil.ReadFile(path.Join(procRoot, "stat"))
+	if err != nil {
+		return stat, err
+	}
+	for _, line := range strings.Split(string(data), "\n") {
+		if strings.HasPrefix(line, "cpu ") {
+			parts := strings.Fields(line)
+			if stat.TotalUsage.User, err = strconv.ParseFloat(parts[1], 64); err != nil {
+				return stat, err
+			}
+			if stat.TotalUsage.Nice, err = strconv.ParseFloat(parts[2], 64); err != nil {
+				return stat, err
+			}
+			if stat.TotalUsage.System, err = strconv.ParseFloat(parts[3], 64); err != nil {
+				return stat, err
+			}
+			if stat.TotalUsage.Idle, err = strconv.ParseFloat(parts[4], 64); err != nil {
+				return stat, err
+			}
+			if stat.TotalUsage.IoWait, err = strconv.ParseFloat(parts[5], 64); err != nil {
+				return stat, err
+			}
+			if stat.TotalUsage.Irq, err = strconv.ParseFloat(parts[6], 64); err != nil {
+				return stat, err
+			}
+			if stat.TotalUsage.SoftIrq, err = strconv.ParseFloat(parts[7], 64); err != nil {
+				return stat, err
+			}
+			if stat.TotalUsage.Steal, err = strconv.ParseFloat(parts[8], 64); err != nil {
+				return stat, err
+			}
+		} else if cpuCorePrefix.MatchString(line) {
+			stat.LogicalCores++
+		}
+	}
+	stat.TotalUsage.User /= CLOCKS_PER_SEC
+	stat.TotalUsage.Nice /= CLOCKS_PER_SEC
+	stat.TotalUsage.System /= CLOCKS_PER_SEC
+	stat.TotalUsage.Idle /= CLOCKS_PER_SEC
+	stat.TotalUsage.IoWait /= CLOCKS_PER_SEC
+	stat.TotalUsage.Irq /= CLOCKS_PER_SEC
+	stat.TotalUsage.SoftIrq /= CLOCKS_PER_SEC
+	stat.TotalUsage.Steal /= CLOCKS_PER_SEC
+	return stat, nil
+}

+ 28 - 0
node/cpu_test.go

@@ -0,0 +1,28 @@
+package node
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestNode_cpu(t *testing.T) {
+	usage, err := cpuStat("fixtures/proc")
+
+	assert.Nil(t, err)
+	//cpu  22246621850 266246696 6211649668 51164293943 1715028476 0 3050509822 0 0 0
+	assert.Equal(t,
+		CpuStat{
+			TotalUsage: CpuUsage{
+				User:    222466218.50,
+				Nice:    2662466.96,
+				System:  62116496.68,
+				Idle:    511642939.43,
+				IoWait:  17150284.76,
+				Irq:     0,
+				SoftIrq: 30505098.22,
+				Steal:   0,
+			},
+			LogicalCores: 16,
+		},
+		usage)
+}

+ 107 - 0
node/disk.go

@@ -0,0 +1,107 @@
+package node
+
+import (
+	"io/ioutil"
+	"k8s.io/klog/v2"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+var blockDevice = regexp.MustCompile(`^(dm-\d+|(s|h|xv|v)d[a-z]|md\d+|nvme\d+n\d+)`)
+
+type DevStat struct {
+	Name             string
+	MajorMinor       string
+	ReadOps          float64
+	WriteOps         float64
+	BytesRead        float64
+	BytesWritten     float64
+	ReadTimeSeconds  float64
+	WriteTimeSeconds float64
+	IoTimeSeconds    float64
+}
+
+type Disks struct {
+	byMajorMinor map[string]DevStat
+}
+
+func (disks *Disks) BlockDevices() []DevStat {
+	var res []DevStat
+	for _, d := range disks.byMajorMinor {
+		groups := blockDevice.FindStringSubmatch(d.Name)
+		if len(groups) < 2 {
+			continue
+		}
+		if groups[1] == d.Name {
+			res = append(res, d)
+		}
+	}
+	return res
+}
+
+func (disks *Disks) GetParentBlockDevice(majorMinor string) *DevStat {
+	dev, ok := disks.byMajorMinor[majorMinor]
+	if !ok {
+		return nil
+	}
+	groups := blockDevice.FindStringSubmatch(dev.Name)
+	if len(groups) < 2 {
+		return nil
+	}
+	parentName := groups[1]
+	for _, d := range disks.byMajorMinor {
+		if d.Name == parentName {
+			return &d
+		}
+	}
+	return nil
+}
+
+func GetDisks() (*Disks, error) {
+	data, err := ioutil.ReadFile(path.Join(procRoot, "diskstats"))
+	if err != nil {
+		return nil, err
+	}
+	disks := &Disks{
+		byMajorMinor: map[string]DevStat{},
+	}
+	for _, line := range strings.Split(string(data), "\n") {
+		fields := strings.Fields(line)
+		if len(fields) < 14 {
+			continue
+		}
+		deviceName := fields[2]
+		values, err := parseFloats(fields[3:])
+		if err != nil {
+			klog.Warningf(`invalid diskstats line "%s": %s`, line, err)
+			continue
+		}
+		majorMinor := fields[0] + ":" + fields[1]
+		disks.byMajorMinor[majorMinor] = DevStat{
+			Name:             deviceName,
+			MajorMinor:       majorMinor,
+			ReadOps:          values[0],
+			BytesRead:        values[2] * 512,
+			ReadTimeSeconds:  values[3] / 1000,
+			WriteOps:         values[4],
+			BytesWritten:     values[6] * 512,
+			WriteTimeSeconds: values[7] / 1000,
+			IoTimeSeconds:    values[9] / 1000,
+		}
+	}
+	return disks, nil
+}
+
+func parseFloats(input []string) ([]float64, error) {
+	res := make([]float64, len(input))
+	for i, strValue := range input {
+		v, err := strconv.ParseFloat(strValue, 64)
+		if err != nil {
+			return nil, err
+		}
+		res[i] = v
+	}
+	return res, nil
+}

+ 53 - 0
node/disk_test.go

@@ -0,0 +1,53 @@
+package node
+
+import (
+	"github.com/stretchr/testify/assert"
+	"sort"
+	"testing"
+)
+
+func TestGetNodeDisks(t *testing.T) {
+	procRoot = "fixtures"
+	d, err := GetDisks()
+	assert.Nil(t, err)
+	assert.Equal(t,
+		DevStat{
+			Name:             "vda",
+			MajorMinor:       "254:0",
+			ReadOps:          10.,
+			WriteOps:         50.,
+			BytesRead:        30. * 512,
+			BytesWritten:     70. * 512,
+			ReadTimeSeconds:  40. / 1000,
+			WriteTimeSeconds: 80. / 1000,
+			IoTimeSeconds:    100. / 1000,
+		},
+		*d.GetParentBlockDevice("254:0"),
+	)
+	assert.Equal(t,
+		DevStat{
+			Name:             "nvme0n1",
+			MajorMinor:       "259:0",
+			ReadOps:          11146,
+			WriteOps:         2.3639172e+07,
+			BytesRead:        3.60193536e+08,
+			BytesWritten:     3.80286784512e+11,
+			ReadTimeSeconds:  1.614,
+			WriteTimeSeconds: 5380.297,
+			IoTimeSeconds:    26059.968},
+		*d.GetParentBlockDevice("259:4"),
+	)
+	names := func(devices []DevStat) []string {
+		var res []string
+		for _, d := range devices {
+			res = append(res, d.Name)
+		}
+		sort.Strings(res)
+		return res
+	}
+
+	assert.Equal(t,
+		[]string{"dm-0", "md1", "nvme0n1", "nvme1n1", "sda", "sdb", "vda", "xvda"},
+		names(d.BlockDevices()),
+	)
+}

+ 25 - 0
node/fixtures/diskstats

@@ -0,0 +1,25 @@
+   1       0 ram0 0 0 0 0 0 0 0 0 0 0 0
+   7       0 loop0 1520 0 3900 265 0 0 0 0 0 168 0 0 0 0 0
+   7       1 loop1 77 0 2390 22 0 0 0 0 0 36 0 0 0 0 0
+   7       2 loop2 1255 0 3377 199 0 0 0 0 0 96 0 0 0 0 0
+   7       3 loop3 9421 0 19709 490 0 0 0 0 0 592 0 0 0 0 0
+   7       4 loop4 4 0 8 0 0 0 0 0 0 4 0 0 0 0 0
+   7       5 loop5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+   7       6 loop6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+   7       7 loop7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 259       0 nvme0n1 11146 6105 703503 1614 23639172 7960632 742747626 5380297 0 26059968 402136 5758 0 967959488 5753
+ 259       1 nvme0n1p1 273 0 14416 39 2 0 2 0 0 144 0 3 0 1028488 1
+ 259       2 nvme0n1p2 10219 6105 654412 1509 15875250 7960632 742747624 3119250 0 24574672 387804 5755 0 966931000 5751
+ 259       3 nvme0n1p3 244 0 18690 9 0 0 0 0 0 100 0 0 0 0 0
+ 259       4 nvme0n1p4 188 0 7714 24 0 0 0 0 0 136 0 0 0 0 0
+ 259       5 nvme1n1 758 0 19996 72 5946004 1486420 540514653 597208 0 1747720 88360 18230 0 1978953016 7074
+  11       0 sr0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+   8       0 sda 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+   8      16 sdb 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+   9       1 md1 43674393 0 1796434600 0 3610616456 0 119095117160 0 0 0 0
+ 254       0 vda 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150
+ 254       1 vda1 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140
+ 111       0 xvda 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 111       1 xvda1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 252       0 dm-0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
+   2       0 fd0 0 0 0 0 0 0 0 0 0 0 0

+ 46 - 0
node/fixtures/proc/meminfo

@@ -0,0 +1,46 @@
+MemTotal:       65871236 kB
+MemFree:         7540732 kB
+MemAvailable:   23826720 kB
+Buffers:           54332 kB
+Cached:         15878036 kB
+SwapCached:            0 kB
+Active:         46981872 kB
+Inactive:        8020476 kB
+Active(anon):   39518496 kB
+Inactive(anon):   271464 kB
+Active(file):    7463376 kB
+Inactive(file):  7749012 kB
+Unevictable:           0 kB
+Mlocked:               0 kB
+SwapTotal:             0 kB
+SwapFree:              0 kB
+Dirty:             71336 kB
+Writeback:             0 kB
+AnonPages:      39073220 kB
+Mapped:          5954828 kB
+Shmem:            717008 kB
+Slab:            2069500 kB
+SReclaimable:    1609620 kB
+SUnreclaim:       459880 kB
+KernelStack:       50000 kB
+PageTables:       726204 kB
+NFS_Unstable:          0 kB
+Bounce:                0 kB
+WritebackTmp:          0 kB
+CommitLimit:    32935616 kB
+Committed_AS:   53031388 kB
+VmallocTotal:   34359738367 kB
+VmallocUsed:           0 kB
+VmallocChunk:          0 kB
+HardwareCorrupted:     0 kB
+AnonHugePages:  20899840 kB
+CmaTotal:              0 kB
+CmaFree:               0 kB
+HugePages_Total:       0
+HugePages_Free:        0
+HugePages_Rsvd:        0
+HugePages_Surp:        0
+Hugepagesize:       2048 kB
+DirectMap4k:    29097604 kB
+DirectMap2M:    36857856 kB
+DirectMap1G:     3145728 kB

+ 24 - 0
node/fixtures/proc/stat

@@ -0,0 +1,24 @@
+cpu  22246621850 266246696 6211649668 51164293943 1715028476 0 3050509822 0 0 0
+cpu0 1543422279 20895200 438055236 3087256727 126322149 0 52756146 0 0 0
+cpu1 1600062044 18951281 456211642 2840753033 132040823 0 219713425 0 0 0
+cpu2 1598760272 18646718 455378534 2852033785 126594632 0 215511298 0 0 0
+cpu3 1586199256 19582560 452272830 2888083685 126328549 0 194475887 0 0 0
+cpu4 1594577918 18410410 456425341 2877918296 120333292 0 200331510 0 0 0
+cpu5 1592572518 18035565 456438176 2885522458 117672912 0 197841477 0 0 0
+cpu6 1592139236 17793912 454804823 2890818120 115139480 0 197725079 0 0 0
+cpu7 1591064647 17406526 454995142 2892781878 113911565 0 197545440 0 0 0
+cpu8 1249900612 34400332 343508568 3226904532 186074121 0 275667466 0 0 0
+cpu9 1167860203 14627028 318816554 3549276097 89397959 0 176872560 0 0 0
+cpu10 1168348820 16200458 319222117 3536901440 96956162 0 178937303 0 0 0
+cpu11 1195687777 12707403 324995522 3516198341 74488164 0 193566037 0 0 0
+cpu12 1191872279 9807246 319970717 3529026747 73160719 0 187456057 0 0 0
+cpu13 1190574834 9461684 320009326 3533460676 70651652 0 187194080 0 0 0
+cpu14 1191677060 9887973 320356800 3527644301 75343164 0 186041443 0 0 0
+cpu15 1191902089 9432394 320188334 3529713822 70613126 0 188874610 0 0 0
+intr 143548743422 33 0 0 8 13 0 0 0 1 1 0 0 0 0 0 0 0 0 44 0 0 0 0 0 0 88 1623497348 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3687265788 2968062821 3669824941 3262065551 1334440618 1023501712 406331773 929802773 1898551167 1059057413 1461867634 1057931840 2836706124 3156858165 2791513665 267951442 514627 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ctxt 10564553913423
+btime 1552305792
+processes 1706743681
+procs_running 6
+procs_blocked 0
+softirq 212693418648 5 1673149098 942230592 1040071743 871818891 0 1374525575 464076855 0 169115681

+ 43 - 0
node/memory.go

@@ -0,0 +1,43 @@
+package node
+
+import (
+	"io/ioutil"
+	"k8s.io/klog/v2"
+	"path"
+	"strconv"
+	"strings"
+)
+
+func memoryInfo(procRoot string) (MemoryStat, error) {
+	mem := MemoryStat{}
+
+	data, err := ioutil.ReadFile(path.Join(procRoot, "meminfo"))
+	if err != nil {
+		return mem, err
+	}
+	for _, line := range strings.Split(string(data), "\n") {
+		parts := strings.Fields(line)
+		if len(parts) < 2 {
+			continue
+		}
+		mul := float64(1)
+		if len(parts) == 3 && parts[2] == "kB" {
+			mul = 1000
+		}
+		v, err := strconv.ParseFloat(parts[1], 64)
+		if err != nil {
+			klog.Warningln("broken meminfo line:", line)
+		}
+		switch parts[0] {
+		case "MemTotal:":
+			mem.TotalBytes = v * mul
+		case "MemFree:":
+			mem.FreeBytes = v * mul
+		case "MemAvailable:":
+			mem.AvailableBytes = v * mul
+		case "Cached:":
+			mem.CachedBytes = v * mul
+		}
+	}
+	return mem, nil
+}

+ 20 - 0
node/memory_test.go

@@ -0,0 +1,20 @@
+package node
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestNode_memory(t *testing.T) {
+	m, err := memoryInfo("fixtures/proc")
+	assert.Nil(t, err)
+	assert.Equal(t,
+		MemoryStat{
+			TotalBytes:     65871236 * 1000,
+			FreeBytes:      7540732 * 1000,
+			AvailableBytes: 23826720 * 1000,
+			CachedBytes:    15878036 * 1000,
+		},
+		m,
+	)
+}

+ 56 - 0
node/metadata/aws.go

@@ -0,0 +1,56 @@
+package metadata
+
+import (
+	"context"
+	"encoding/json"
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
+	"io"
+	"k8s.io/klog/v2"
+)
+
+func getAwsMetadata() *CloudMetadata {
+	ctx, cancel := context.WithTimeout(context.Background(), metadataServiceTimeout)
+	defer cancel()
+	cfg, err := config.LoadDefaultConfig(ctx)
+	if err != nil {
+		klog.Errorln(err)
+		return nil
+	}
+	c := imds.NewFromConfig(cfg)
+	md := &CloudMetadata{
+		Provider:           CloudProviderAWS,
+		InstanceId:         getAwsMetadataVariable(ctx, c, "instance-id"),
+		LifeCycle:          getAwsMetadataVariable(ctx, c, "instance-life-cycle"),
+		InstanceType:       getAwsMetadataVariable(ctx, c, "instance-type"),
+		Region:             getAwsMetadataVariable(ctx, c, "placement/region"),
+		AvailabilityZone:   getAwsMetadataVariable(ctx, c, "placement/availability-zone"),
+		AvailabilityZoneId: getAwsMetadataVariable(ctx, c, "placement/availability-zone-id"),
+		LocalIPv4:          getAwsMetadataVariable(ctx, c, "local-ipv4"),
+		PublicIPv4:         getAwsMetadataVariable(ctx, c, "public-ipv4"),
+	}
+	if infoJson := getAwsMetadataVariable(ctx, c, "identity-credentials/ec2/info"); infoJson != "" {
+		m := map[string]string{}
+		if err := json.Unmarshal([]byte(infoJson), &m); err != nil {
+			klog.Errorln(err)
+		} else {
+			md.AccountId = m["AccountId"]
+		}
+	}
+	return md
+}
+
+func getAwsMetadataVariable(ctx context.Context, client *imds.Client, path string) string {
+	res, err := client.GetMetadata(ctx, &imds.GetMetadataInput{Path: path})
+	if err != nil {
+		klog.Errorln(path, err)
+		return ""
+	}
+	defer res.Content.Close()
+	payload, err := io.ReadAll(res.Content)
+	if err != nil {
+		klog.Errorln(path, err)
+		return ""
+	}
+	return string(payload)
+}

+ 82 - 0
node/metadata/azure.go

@@ -0,0 +1,82 @@
+package metadata
+
+import (
+	"encoding/json"
+	"k8s.io/klog/v2"
+	"net/http"
+)
+
+const (
+	azureEndpoint = "http://169.254.169.254/metadata/instance"
+)
+
+type azureIp struct {
+	Private string `json:"privateIpAddress"`
+	Public  string `json:"publicIpAddress"`
+}
+
+type azureInterface struct {
+	Ipv4 struct {
+		IpAddress []azureIp `json:"ipAddress"`
+	}
+}
+
+type azureInstanceMetadata struct {
+	Compute struct {
+		Region         string `json:"location"`
+		Id             string `json:"vmID"`
+		Type           string `json:"vmSize"`
+		Zone           string `json:"zone"`
+		SubscriptionId string `json:"subscriptionId"`
+	}
+	Network struct {
+		Interface []azureInterface `json:"interface"`
+	}
+}
+
+func getAzureMetadata() *CloudMetadata {
+	req, err := http.NewRequest(http.MethodGet, azureEndpoint, nil)
+	if err != nil {
+		klog.Errorln(err)
+		return nil
+	}
+	req.Header.Add("Metadata", "True")
+	q := req.URL.Query()
+	q.Add("format", "json")
+	q.Add("api-version", "2021-05-01")
+	req.URL.RawQuery = q.Encode()
+
+	client := http.DefaultClient
+	client.Timeout = metadataServiceTimeout
+
+	resp, err := client.Do(req)
+	if err != nil {
+		klog.Errorln(err)
+		return nil
+	}
+	if resp.StatusCode != 200 {
+		klog.Errorln("metadata service response:", resp.Status)
+		return nil
+	}
+	defer resp.Body.Close()
+
+	instanceMd := &azureInstanceMetadata{}
+	decoder := json.NewDecoder(resp.Body)
+	if err := decoder.Decode(instanceMd); err != nil {
+		klog.Errorln("failed to unmarshall response of Azure metadata service:", err)
+		return nil
+	}
+	md := &CloudMetadata{
+		Provider:         CloudProviderAzure,
+		AccountId:        instanceMd.Compute.SubscriptionId,
+		InstanceId:       instanceMd.Compute.Id,
+		InstanceType:     instanceMd.Compute.Type,
+		Region:           instanceMd.Compute.Region,
+		AvailabilityZone: instanceMd.Compute.Zone,
+	}
+	if len(instanceMd.Network.Interface) > 0 && len(instanceMd.Network.Interface[0].Ipv4.IpAddress) > 0 {
+		md.LocalIPv4 = instanceMd.Network.Interface[0].Ipv4.IpAddress[0].Private
+		md.PublicIPv4 = instanceMd.Network.Interface[0].Ipv4.IpAddress[0].Public
+	}
+	return md
+}

+ 49 - 0
node/metadata/gcp.go

@@ -0,0 +1,49 @@
+package metadata
+
+import (
+	gcp "cloud.google.com/go/compute/metadata"
+	"k8s.io/klog/v2"
+	"net/http"
+	"strings"
+)
+
+func getGcpMetadata() *CloudMetadata {
+	hc := http.DefaultClient
+	hc.Timeout = metadataServiceTimeout
+	c := gcp.NewClient(hc)
+	md := &CloudMetadata{
+		Provider:   CloudProviderGCP,
+		AccountId:  getGcpMetadataVariable(c, "project/project-id"),
+		InstanceId: getGcpMetadataVariable(c, "instance/id"),
+		LocalIPv4:  getGcpMetadataVariable(c, "instance/network-interfaces/0/ip"),
+		PublicIPv4: getGcpMetadataVariable(c, "instance/network-interfaces/0/access-configs/0/external-ip"),
+	}
+	switch strings.ToLower(getGcpMetadataVariable(c, "instance/scheduling/preemptible")) {
+	case "false":
+		md.LifeCycle = "on-demand"
+	case "true":
+		md.LifeCycle = "preemptible"
+	}
+
+	// projects/PROJECT_NUM/machineTypes/MACHINE_TYPE
+	if parts := strings.SplitN(getGcpMetadataVariable(c, "instance/machine-type"), "/", 4); len(parts) == 4 {
+		md.InstanceType = parts[3]
+	}
+
+	// projects/PROJECT_NUM/zones/ZONE
+	if parts := strings.SplitN(getGcpMetadataVariable(c, "instance/zone"), "/", 4); len(parts) == 4 {
+		md.AvailabilityZone = parts[3]
+		if idx := strings.LastIndex(md.AvailabilityZone, "-"); idx != -1 {
+			md.Region = md.AvailabilityZone[:idx]
+		}
+	}
+	return md
+}
+
+func getGcpMetadataVariable(client *gcp.Client, path string) string {
+	s, err := client.Get(path)
+	if err != nil {
+		klog.Errorln(path, err)
+	}
+	return s
+}

+ 65 - 0
node/metadata/metadata.go

@@ -0,0 +1,65 @@
+package metadata
+
+import (
+	"k8s.io/klog/v2"
+	"os"
+	"strings"
+	"time"
+)
+
+const metadataServiceTimeout = 5 * time.Second
+
+type CloudProvider string
+
+const (
+	CloudProviderAWS     CloudProvider = "AWS"
+	CloudProviderGCP     CloudProvider = "GCP"
+	CloudProviderAzure   CloudProvider = "Azure"
+	CloudProviderUnknown CloudProvider = ""
+)
+
+type CloudMetadata struct {
+	Provider           CloudProvider
+	AccountId          string
+	InstanceId         string
+	InstanceType       string
+	LifeCycle          string
+	Region             string
+	AvailabilityZone   string
+	AvailabilityZoneId string
+	LocalIPv4          string
+	PublicIPv4         string
+}
+
+func getCloudProvider() CloudProvider {
+	if d, err := os.ReadFile("/sys/hypervisor/uuid"); err == nil { // AWS Xen instances
+		if strings.HasPrefix(strings.ToLower(string(d)), "ec2") {
+			return CloudProviderAWS
+		}
+	}
+	if vendor, err := os.ReadFile("/sys/class/dmi/id/board_vendor"); err == nil {
+		switch strings.TrimSpace(string(vendor)) {
+		case "Amazon EC2":
+			return CloudProviderAWS
+		case "Google":
+			return CloudProviderGCP
+		case "Microsoft Corporation":
+			return CloudProviderAzure
+		}
+	}
+	return CloudProviderUnknown
+}
+
+func GetInstanceMetadata() *CloudMetadata {
+	provider := getCloudProvider()
+	klog.Infoln("cloud provider:", provider)
+	switch provider {
+	case CloudProviderAWS:
+		return getAwsMetadata()
+	case CloudProviderGCP:
+		return getGcpMetadata()
+	case CloudProviderAzure:
+		return getAzureMetadata()
+	}
+	return nil
+}

+ 69 - 0
node/net.go

@@ -0,0 +1,69 @@
+package node
+
+import (
+	"github.com/coroot/coroot-node-agent/proc"
+	"github.com/vishvananda/netlink"
+	"golang.org/x/sys/unix"
+	"regexp"
+)
+
+var includeNetDev = regexp.MustCompile(`^(enp\d+s\d+(f\d+)?|eth\d+|eno\d+|ens\d+)`)
+
+type NetDeviceInfo struct {
+	Name      string
+	Up        float64
+	Addresses []string
+	RxBytes   float64
+	TxBytes   float64
+	RxPackets float64
+	TxPackets float64
+}
+
+func netDevices() ([]NetDeviceInfo, error) {
+	hostNs, err := proc.GetHostNetNs()
+	if err != nil {
+		return nil, err
+	}
+	defer hostNs.Close()
+	h, err := netlink.NewHandleAt(hostNs)
+	if err != nil {
+		return nil, err
+	}
+	defer h.Delete()
+	links, err := h.LinkList()
+	if err != nil {
+		return nil, err
+	}
+	var res []NetDeviceInfo
+	for _, link := range links {
+		attrs := link.Attrs()
+		if !includeNetDev.MatchString(attrs.Name) {
+			continue
+		}
+		info := NetDeviceInfo{
+			Name:      attrs.Name,
+			RxBytes:   float64(attrs.Statistics.RxBytes),
+			TxBytes:   float64(attrs.Statistics.TxBytes),
+			RxPackets: float64(attrs.Statistics.RxPackets),
+			TxPackets: float64(attrs.Statistics.TxPackets),
+		}
+		if attrs.OperState == netlink.OperUp {
+			info.Up = 1
+		}
+
+		addrs, err := h.AddrList(link, unix.AF_UNSPEC)
+		if err != nil {
+			return nil, err
+		}
+		for _, addr := range addrs {
+			ip := addr.IP
+			if ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsLinkLocalMulticast() {
+				continue
+			}
+			info.Addresses = append(info.Addresses, addr.IP.String())
+		}
+		res = append(res, info)
+	}
+	return res, nil
+
+}

+ 254 - 0
pinger/pinger.go

@@ -0,0 +1,254 @@
+package pinger
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"github.com/coroot/coroot-node-agent/proc"
+	"github.com/vishvananda/netns"
+	"golang.org/x/net/icmp"
+	"golang.org/x/net/ipv4"
+	"inet.af/netaddr"
+	"k8s.io/klog/v2"
+	"net"
+	"os"
+	"strings"
+	"syscall"
+	"time"
+)
+
+const (
+	pingReplyPollTimeout         = 10 * time.Millisecond
+	protocolICMP                 = 1 // Internet Control Message
+	SOF_TIMESTAMPING_TX_SOFTWARE = 1 << 1
+	SOF_TIMESTAMPING_TX_SCHED    = 1 << 8
+	SOF_TIMESTAMPING_RX_SOFTWARE = 1 << 3
+)
+
+var (
+	pingerID = os.Getpid() & 0xFFFF
+)
+
+type sentPacket struct {
+	seq         int
+	txTimestamp time.Time
+}
+
+func Ping(ns netns.NsHandle, originNs netns.NsHandle, targets []netaddr.IP, timeout time.Duration) (map[netaddr.IP]float64, error) {
+	if len(targets) < 1 {
+		return nil, nil
+	}
+	var conn *net.IPConn
+	err := proc.ExecuteInNetNs(ns, originNs, func() error {
+		c, err := openConn()
+		if err != nil {
+			return err
+		}
+		conn = c
+		return nil
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to open IPConn: %s", err)
+	}
+	defer conn.Close()
+	f, err := conn.File()
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	fd := int(f.Fd())
+
+	ids := make(map[netaddr.IP]*sentPacket, len(targets))
+	for seq, ip := range targets {
+		pkt := &sentPacket{seq: seq + 1, txTimestamp: time.Now()}
+		if err := send(conn, pkt.seq, ip.IPAddr()); err != nil {
+			if strings.HasPrefix(err.Error(), "resource temporarily unavailable") {
+				continue
+			}
+			return nil, fmt.Errorf("failed to send packet to %s: %s", ip, err)
+		}
+		if pkt.txTimestamp, err = getTxTimestamp(fd); err != nil {
+			if strings.HasPrefix(err.Error(), "resource temporarily unavailable") {
+				continue
+			}
+			return nil, fmt.Errorf("failed to get RX timestamp: %s", err)
+		}
+		ids[ip] = pkt
+	}
+
+	timeoutTicker := time.NewTimer(timeout)
+	defer timeoutTicker.Stop()
+
+	rttByIp := make(map[netaddr.IP]float64, len(targets))
+	for {
+		select {
+		case <-timeoutTicker.C:
+			return rttByIp, nil
+		default:
+			if len(rttByIp) == len(targets) {
+				return rttByIp, nil
+			}
+			remoteAddr, echoReply, rxTimestamp, err := receive(conn)
+			if err != nil {
+				if !strings.Contains(err.Error(), "interrupted system call") { // recvmsg timeout is not an issue
+					klog.Errorln(err)
+				}
+				continue
+			}
+			if echoReply == nil {
+				continue
+			}
+			if echoReply.ID != pingerID {
+				continue
+			}
+			ip, ok := netaddr.FromStdIP(remoteAddr.IP)
+			if !ok {
+				continue
+			}
+			if pkt, ok := ids[ip]; ok && pkt.seq == echoReply.Seq {
+				rtt := rxTimestamp.Sub(pkt.txTimestamp).Seconds()
+				if rtt < 0 { // a small negative value is possible if the clock has adjusted by ntpd
+					rtt = 0
+				}
+				rttByIp[ip] = rtt
+			}
+		}
+	}
+}
+
+func send(conn *net.IPConn, seq int, ip net.Addr) error {
+	msg := &icmp.Message{
+		Type: ipv4.ICMPTypeEcho,
+		Body: &icmp.Echo{
+			ID:  pingerID,
+			Seq: seq,
+		},
+	}
+	data, err := msg.Marshal(nil)
+	if err != nil {
+		return err
+	}
+	_, err = conn.WriteTo(data, ip)
+	return err
+}
+
+func getTxTimestamp(socketFd int) (time.Time, error) {
+	var t time.Time
+	pktBuf := make([]byte, 1024)
+	oob := make([]byte, 1024)
+	_, oobn, _, _, err := syscall.Recvmsg(socketFd, pktBuf, oob, syscall.MSG_ERRQUEUE)
+	if err != nil {
+		return t, err
+	}
+	cms, err := syscall.ParseSocketControlMessage(oob[:oobn])
+	if err != nil {
+		return t, err
+	}
+	for _, cm := range cms {
+		if cm.Header.Level == syscall.SOL_SOCKET && cm.Header.Type == syscall.SO_TIMESTAMP {
+			var tv syscall.Timeval
+			if err := binary.Read(bytes.NewBuffer(cm.Data), binary.LittleEndian, &tv); err != nil {
+				return t, err
+			}
+			return time.Unix(tv.Unix()), nil
+		}
+	}
+	return t, errors.New("empty response")
+}
+
+func receive(conn *net.IPConn) (*net.IPAddr, *icmp.Echo, time.Time, error) {
+	pktBuf := make([]byte, 1024)
+	oob := make([]byte, 1024)
+	var ts time.Time
+
+	_ = conn.SetReadDeadline(time.Now().Add(pingReplyPollTimeout))
+	n, oobn, _, ra, err := conn.ReadMsgIP(pktBuf, oob)
+	if err != nil {
+		if neterr, ok := err.(*net.OpError); ok && neterr.Timeout() {
+			return nil, nil, ts, nil
+		}
+		if strings.Contains(err.Error(), "no message of desired type") {
+			return nil, nil, ts, nil
+		}
+		return nil, nil, ts, err
+	}
+
+	if ts, err = getRxTimestamp(oob, oobn); err != nil {
+		return nil, nil, ts, fmt.Errorf("failed to get RX timestamp: %s", err)
+	}
+
+	echo, err := extractEchoFromPacket(pktBuf, n)
+	if err != nil {
+		return nil, nil, ts, fmt.Errorf("failed to extract ICMP Echo from IPv4 packet %s: %s", ra, err)
+	}
+	return ra, echo, ts, nil
+}
+
+func extractEchoFromPacket(pktBuf []byte, n int) (*icmp.Echo, error) {
+	if n < ipv4.HeaderLen {
+		return nil, errors.New("malformed IPv4 packet")
+	}
+	pktBuf = pktBuf[ipv4.HeaderLen:]
+	var m *icmp.Message
+	m, err := icmp.ParseMessage(protocolICMP, pktBuf)
+	if err != nil {
+		return nil, err
+	}
+	if m.Type != ipv4.ICMPTypeEchoReply {
+		return nil, nil
+	}
+	echo, ok := m.Body.(*icmp.Echo)
+	if !ok {
+		return nil, fmt.Errorf("malformed ICMP message body: %T", m.Body)
+	}
+	return echo, nil
+}
+
+func getRxTimestamp(oob []byte, oobn int) (time.Time, error) {
+	var ts time.Time
+	cms, err := syscall.ParseSocketControlMessage(oob[:oobn])
+	if err != nil {
+		return ts, err
+	}
+	for _, cm := range cms {
+		if cm.Header.Level == syscall.SOL_SOCKET && cm.Header.Type == syscall.SO_TIMESTAMP {
+			var tv syscall.Timeval
+			if err := binary.Read(bytes.NewBuffer(cm.Data), binary.LittleEndian, &tv); err != nil {
+				return ts, err
+			}
+			return time.Unix(tv.Unix()), nil
+		}
+	}
+	return ts, errors.New("invalid out-of-band data")
+}
+
+func openConn() (*net.IPConn, error) {
+	conn, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
+	if err != nil {
+		return nil, err
+	}
+	ipconn := conn.(*net.IPConn)
+	f, err := ipconn.File()
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	fd := int(f.Fd())
+
+	flags := SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_TX_SCHED | SOF_TIMESTAMPING_RX_SOFTWARE
+	if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TIMESTAMPING, flags); err != nil {
+		return nil, err
+	}
+	if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TIMESTAMP, 1); err != nil {
+		return nil, err
+	}
+	timeout := syscall.Timeval{Sec: 0, Usec: 1000}
+	if err := syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &timeout); err != nil {
+		return nil, err
+	}
+	if err := syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &timeout); err != nil {
+		return nil, err
+	}
+	return ipconn, nil
+}

+ 38 - 0
proc/fd.go

@@ -0,0 +1,38 @@
+package proc
+
+import (
+	"os"
+	"strconv"
+	"strings"
+)
+
+type FdInfo struct {
+	MntId string
+	Flags int
+	Dest  string
+}
+
+func GetFdInfo(pid uint32, fd uint32) *FdInfo {
+	fds := strconv.Itoa(int(fd))
+	data, err := os.ReadFile(Path(pid, "fdinfo", fds))
+	if err != nil {
+		return nil
+	}
+	dest, err := os.Readlink(Path(pid, "fd", fds))
+	if err != nil {
+		return nil
+	}
+	res := FdInfo{Dest: dest}
+	for _, line := range strings.Split(string(data), "\n") {
+		if strings.HasPrefix(line, "mnt_id:") {
+			res.MntId = strings.TrimSpace(strings.TrimPrefix(line, "mnt_id:"))
+			continue
+		}
+		if strings.HasPrefix(line, "flags:") {
+			flags, _ := strconv.ParseInt(strings.TrimSpace(strings.TrimPrefix(line, "flags:")), 8, 32)
+			res.Flags = int(flags)
+			continue
+		}
+	}
+	return &res
+}

+ 1 - 0
proc/fixtures/123/fd/4

@@ -0,0 +1 @@
+/var/lib/postgresql/data/pg_wal/000000010000000000000001

+ 3 - 0
proc/fixtures/123/fdinfo/4

@@ -0,0 +1,3 @@
+pos:	0
+flags:	0100002
+mnt_id:	1965

+ 39 - 0
proc/fixtures/123/mountinfo

@@ -0,0 +1,39 @@
+3106 2926 0:328 / / rw,relatime master:837 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/M6WZCEZRHUXPOUB5L2NRWN2WUO:/var/lib/docker/overlay2/l/PSK625Y3NIGFHS26AUKE2KRMA2:/var/lib/docker/overlay2/l/PT66PDXGNEH225Q3FYX7Z2UV6L:/var/lib/docker/overlay2/l/35H7IJBYGOO6ILXKMRSGDXVNNF:/var/lib/docker/overlay2/l/5DCUF747JZCYYVPTGZL6CHP5U5:/var/lib/docker/overlay2/l/7SPZ3W6RP7OPEGYKKVSRKZ3JXT:/var/lib/docker/overlay2/l/SGMSFRI3ICWH3OCUY32IHUGB5T:/var/lib/docker/overlay2/l/ZKB4EVRT44YL4EW6RVR7GKUK7G:/var/lib/docker/overlay2/l/PADSNYEAQGS6W76AJOYSZ2NYQJ:/var/lib/docker/overlay2/l/DGQRTF5TBDCTX7BSDFQEHLRWC2:/var/lib/docker/overlay2/l/C2WHETJNEN5LLJRVWZJCFQI4QY:/var/lib/docker/overlay2/l/PXCA2NWD6RAZXQHGQ7ENSNDQ6A:/var/lib/docker/overlay2/l/GUWDUKNW2WIFXIO7PYJRGUVNGD:/var/lib/docker/overlay2/l/PPAC3QHMX6BMD7MLLB4ZNGC72X,upperdir=/var/lib/docker/overlay2/cff76cd160f34ad2c4d28401c38c97f816c7f29871b8f43f93c5a880caedc2ea/diff,workdir=/var/lib/docker/overlay2/cff76cd160f34ad2c4d28401c38c97f816c7f29871b8f43f93c5a880caedc2ea/work,xino=off
+3107 3106 0:329 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
+3108 3106 0:330 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+3109 3108 0:331 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
+3110 3106 0:315 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
+3111 3110 0:332 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
+3112 3111 0:30 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd
+3113 3111 0:34 / /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma
+3114 3111 0:35 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,devices
+3115 3111 0:36 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,net_cls,net_prio
+3116 3111 0:37 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,cpu,cpuacct
+3117 3111 0:38 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb
+3118 3111 0:39 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,pids
+3119 3111 0:40 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,cpuset
+3120 3111 0:41 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,memory
+3121 3111 0:42 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer
+3122 3111 0:43 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,blkio
+3123 3111 0:44 /kubepods/burstable/pod51d1885e-187c-4563-a51e-2cccf685610c/794b58c8a6dff2d8313df105c011868f33584884d6dd3384a5ebc570fd6b8b9b /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,perf_event
+3124 3108 0:311 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
+3125 3108 259:2 /var/lib/kubelet/pods/51d1885e-187c-4563-a51e-2cccf685610c/containers/kafka/628906d4 /dev/termination-log rw,relatime - ext4 /dev/nvme0n1p2 rw
+3126 3106 259:2 /mnt/nvme0/vol3 /bitnami/kafka rw,relatime - ext4 /dev/nvme0n1p2 rw
+3127 3106 259:2 /var/lib/kubelet/pods/51d1885e-187c-4563-a51e-2cccf685610c/volumes/kubernetes.io~configmap/scripts/..2020_11_24_08_56_47.305546261/setup.sh /scripts/setup.sh ro,relatime - ext4 /dev/nvme0n1p2 rw
+3128 3106 259:2 /var/lib/docker/containers/590f851f378ca534e46933199a7e7a914f4d23c125612753765acd709dfaceb6/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/nvme0n1p2 rw
+3129 3106 259:2 /var/lib/docker/containers/590f851f378ca534e46933199a7e7a914f4d23c125612753765acd709dfaceb6/hostname /etc/hostname rw,relatime - ext4 /dev/nvme0n1p2 rw
+3130 3106 259:2 /var/lib/kubelet/pods/51d1885e-187c-4563-a51e-2cccf685610c/etc-hosts /etc/hosts rw,relatime - ext4 /dev/nvme0n1p2 rw
+3131 3108 0:310 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k
+3132 3106 0:308 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw
+2927 3107 0:329 /bus /proc/bus ro,relatime - proc proc rw
+2928 3107 0:329 /fs /proc/fs ro,relatime - proc proc rw
+2929 3107 0:329 /irq /proc/irq ro,relatime - proc proc rw
+2930 3107 0:329 /sys /proc/sys ro,relatime - proc proc rw
+2931 3107 0:329 /sysrq-trigger /proc/sysrq-trigger ro,relatime - proc proc rw
+2932 3107 0:333 / /proc/acpi ro,relatime - tmpfs tmpfs ro
+2933 3107 0:330 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+2934 3107 0:330 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+2935 3107 0:330 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+2936 3107 0:330 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+2937 3107 0:334 / /proc/scsi ro,relatime - tmpfs tmpfs ro
+2977 3110 0:335 / /sys/firmware ro,relatime - tmpfs tmpfs ro

+ 3 - 0
proc/fixtures/123/net/tcp

@@ -0,0 +1,3 @@
+ sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
+   0: 00000000:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000   999        0 8039432 1 ffff8d4773785780 100 0 0 10 0
+   1: 030011AC:1538 040011AC:8DEC 01 00000000:00000000 02:000AFB60 00000000   999        0 8134154 2 ffff8d455e271180 20 4 20 10 -1

+ 2 - 0
proc/fixtures/123/net/tcp6

@@ -0,0 +1,2 @@
+  sl  local_address                         remote_address                        st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
+   0: 00000000000000000000000000000000:1538 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000   999        0 8039433 1 ffff8d477378d580 100 0 0 10 0

+ 23 - 0
proc/fs.go

@@ -0,0 +1,23 @@
+package proc
+
+import "syscall"
+
+type FSStat struct {
+	CapacityBytes uint64
+	UsedBytes     uint64
+	ReservedBytes uint64
+}
+
+func StatFS(dirPath string) (FSStat, error) {
+	var s syscall.Statfs_t
+
+	if err := syscall.Statfs(dirPath, &s); err != nil {
+		return FSStat{}, err
+	}
+	res := FSStat{
+		CapacityBytes: s.Blocks * uint64(s.Bsize),
+		UsedBytes:     (s.Blocks - s.Bfree) * uint64(s.Bsize),
+		ReservedBytes: (s.Bfree - s.Bavail) * uint64(s.Bsize),
+	}
+	return res, nil
+}

+ 30 - 0
proc/mount.go

@@ -0,0 +1,30 @@
+package proc
+
+import (
+	"os"
+	"strings"
+)
+
+type MountInfo struct {
+	MajorMinor string
+	MountPoint string
+}
+
+func GetMountInfo(pid uint32) map[string]MountInfo {
+	data, err := os.ReadFile(Path(pid, "mountinfo"))
+	if err != nil {
+		return nil
+	}
+	res := map[string]MountInfo{}
+	for _, line := range strings.Split(string(data), "\n") {
+		fields := strings.Fields(line)
+		if len(fields) < 5 {
+			continue
+		}
+		if strings.HasPrefix(fields[2], "0:") {
+			continue
+		}
+		res[fields[0]] = MountInfo{MajorMinor: fields[2], MountPoint: fields[4]}
+	}
+	return res
+}

+ 108 - 0
proc/net.go

@@ -0,0 +1,108 @@
+package proc
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/binary"
+	"encoding/hex"
+	"inet.af/netaddr"
+	"net"
+	"os"
+)
+
+const (
+	stateEstablished = "01"
+	stateListen      = "0A"
+)
+
+type Sock struct {
+	Inode  string
+	SAddr  netaddr.IPPort
+	DAddr  netaddr.IPPort
+	Listen bool
+}
+
+func GetSockets(pid uint32) ([]Sock, error) {
+	var res []Sock
+	var e error
+	for _, f := range []string{"tcp", "tcp6"} {
+		ss, err := readSockets(Path(pid, "net", f))
+		if err != nil {
+			e = err
+		}
+		res = append(res, ss...)
+	}
+	return res, e
+}
+
+func readSockets(src string) ([]Sock, error) {
+	f, err := os.Open(src)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	var res []Sock
+	scanner := bufio.NewScanner(f)
+	header := true
+	for scanner.Scan() {
+		if header {
+			header = false
+			continue
+		}
+		b := scanner.Bytes()
+		_, b = nextField(b)
+		local, b := nextField(b)
+		remote, b := nextField(b)
+		st, b := nextField(b)
+		state := string(st)
+		if state != stateEstablished && state != stateListen {
+			continue
+		}
+		_, b = nextField(b)
+		_, b = nextField(b)
+		_, b = nextField(b)
+		_, b = nextField(b)
+		_, b = nextField(b)
+		inode, b := nextField(b)
+		res = append(res, Sock{SAddr: decodeAddr(local), DAddr: decodeAddr(remote), Listen: state == stateListen, Inode: string(inode)})
+	}
+	return res, nil
+}
+
+func nextField(s []byte) ([]byte, []byte) {
+	for i, b := range s {
+		if b != ' ' {
+			s = s[i:]
+			break
+		}
+	}
+	for i, b := range s {
+		if b == ' ' {
+			return s[:i], s[i:]
+		}
+	}
+	return nil, nil
+}
+
+func decodeAddr(src []byte) netaddr.IPPort {
+	col := bytes.IndexByte(src, ':')
+	if col == -1 || (col != 8 && col != 32) {
+		return netaddr.IPPort{}
+	}
+	ip := make([]byte, col/2)
+	if _, err := hex.Decode(ip, src[:col]); err != nil {
+		return netaddr.IPPort{}
+	}
+	port := make([]byte, 2)
+	if _, err := hex.Decode(port, src[col+1:]); err != nil {
+		return netaddr.IPPort{}
+	}
+	for i, j := 0, len(ip)-1; i < j; i, j = i+1, j-1 {
+		ip[i], ip[j] = ip[j], ip[i]
+	}
+	ipp, ok := netaddr.FromStdIP(net.IP(ip))
+	if !ok {
+		return netaddr.IPPort{}
+	}
+	return netaddr.IPPortFrom(ipp, binary.BigEndian.Uint16(port))
+}

+ 71 - 0
proc/ns.go

@@ -0,0 +1,71 @@
+package proc
+
+import (
+	"github.com/vishvananda/netlink"
+	"github.com/vishvananda/netns"
+	"golang.org/x/sys/unix"
+	"inet.af/netaddr"
+	"runtime"
+)
+
+func GetNetNs(pid uint32) (netns.NsHandle, error) {
+	return netns.GetFromPid(int(pid))
+}
+
+func GetSelfNetNs() (netns.NsHandle, error) {
+	return netns.Get()
+}
+
+func GetHostNetNs() (netns.NsHandle, error) {
+	return GetNetNs(1)
+}
+
+func ExecuteInNetNs(newNs, curNs netns.NsHandle, f func() error) error {
+	if newNs.Equal(curNs) {
+		return f()
+	}
+
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+	if err := netns.Set(newNs); err != nil {
+		return err
+	}
+
+	errF := f()
+
+	if err := netns.Set(curNs); err != nil {
+		return err
+	}
+
+	return errF
+}
+
+func GetNsIps(ns netns.NsHandle) ([]netaddr.IP, error) {
+	h, err := netlink.NewHandleAt(ns)
+	if err != nil {
+		return nil, err
+	}
+	defer h.Delete()
+
+	links, err := h.LinkList()
+	if err != nil {
+		return nil, err
+	}
+	var res []netaddr.IP
+	for _, link := range links {
+		addrs, err := h.AddrList(link, unix.AF_UNSPEC)
+		if err != nil {
+			return nil, err
+		}
+		for _, addr := range addrs {
+			ip := addr.IP
+			if ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsLinkLocalMulticast() {
+				continue
+			}
+			if ip, ok := netaddr.FromStdIP(ip); ok {
+				res = append(res, ip)
+			}
+		}
+	}
+	return res, nil
+}

+ 55 - 0
proc/proc.go

@@ -0,0 +1,55 @@
+package proc
+
+import (
+	"github.com/coroot/coroot-node-agent/cgroup"
+	"os"
+	"path"
+	"strconv"
+)
+
+var root = "/proc"
+
+func Path(pid uint32, subpath ...string) string {
+	return path.Join(append([]string{root, strconv.Itoa(int(pid))}, subpath...)...)
+}
+
+func HostPath(p string) string {
+	return Path(1, "root", p)
+}
+
+func GetCmdline(pid uint32) []byte {
+	cmdline, err := os.ReadFile(Path(pid, "cmdline"))
+	if err != nil {
+		return nil
+	}
+	return cmdline
+}
+
+func ReadCgroup(pid uint32) (*cgroup.Cgroup, error) {
+	return cgroup.NewFromProcessCgroupFile(Path(pid, "cgroup"))
+}
+
+func ListPids() ([]uint32, error) {
+	root, err := os.Open(root)
+	if err != nil {
+		return nil, err
+	}
+	defer root.Close()
+	dirs, err := root.Readdirnames(0)
+	if err != nil {
+		return nil, err
+	}
+	res := make([]uint32, 0, len(dirs))
+	for _, dir := range dirs {
+		pid64, err := strconv.ParseUint(dir, 10, 32)
+		if err != nil {
+			continue
+		}
+		res = append(res, uint32(pid64))
+	}
+	return res, nil
+}
+
+func SetRoot(path string) {
+	root = path
+}

+ 57 - 0
proc/proc_test.go

@@ -0,0 +1,57 @@
+package proc
+
+import (
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"inet.af/netaddr"
+	"testing"
+)
+
+func init() {
+	SetRoot("fixtures")
+}
+
+func TestListPids(t *testing.T) {
+	res, err := ListPids()
+	require.NoError(t, err)
+
+	assert.Equal(t, []uint32{123}, res)
+}
+
+func TestGetMountInfo(t *testing.T) {
+	res := GetMountInfo(123)
+	assert.Equal(t, map[string]MountInfo{
+		"3125": {MajorMinor: "259:2", MountPoint: "/dev/termination-log"},
+		"3126": {MajorMinor: "259:2", MountPoint: "/bitnami/kafka"},
+		"3127": {MajorMinor: "259:2", MountPoint: "/scripts/setup.sh"},
+		"3128": {MajorMinor: "259:2", MountPoint: "/etc/resolv.conf"},
+		"3129": {MajorMinor: "259:2", MountPoint: "/etc/hostname"},
+		"3130": {MajorMinor: "259:2", MountPoint: "/etc/hosts"},
+	}, res)
+}
+
+func TestGetFdInfo(t *testing.T) {
+	res := GetFdInfo(123, 4)
+	assert.Equal(t, FdInfo{
+		MntId: "1965",
+		Flags: int(0100002),
+		Dest:  "/var/lib/postgresql/data/pg_wal/000000010000000000000001",
+	}, *res)
+}
+
+func TestGetSockets(t *testing.T) {
+	res, err := GetSockets(123)
+	require.NoError(t, err)
+
+	ipp := func(s string) netaddr.IPPort {
+		res, err := netaddr.ParseIPPort(s)
+		require.NoError(t, err)
+		return res
+	}
+
+	assert.Equal(t, []Sock{
+		{Inode: "8039432", SAddr: ipp("0.0.0.0:5432"), DAddr: ipp("0.0.0.0:0"), Listen: true},
+		{Inode: "8134154", SAddr: ipp("172.17.0.3:5432"), DAddr: ipp("172.17.0.4:36332"), Listen: false},
+		{Inode: "8039433", SAddr: ipp("[::]:5432"), DAddr: ipp("[::]:0"), Listen: true},
+	}, res)
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff