diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ae5db4e..76bea01 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,11 +19,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to Quay.io Container Registry uses: docker/login-action@v1 with: @@ -32,11 +34,11 @@ jobs: password: ${{ secrets.SYSTEM_QUAY_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v4 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . push: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 764bdc0..12e4bd2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,12 +13,12 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - - cron: '30 22 * * 0' + - cron: "30 22 * * 0" jobs: analyze: @@ -32,40 +32,40 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'go' ] + language: ["go"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - - name: Checkout repository - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v3 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/Dockerfile b/Dockerfile index fe06716..2f3ddd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.17-alpine AS build +FROM golang:1.19-alpine AS build WORKDIR /src COPY go.* ./ RUN go mod download @@ -8,7 +8,7 @@ RUN apk --no-cache add git &&\ go test ./... &&\ go build -o /kube-summary-exporter . -FROM alpine:3.13 +FROM alpine:latest COPY --from=build /kube-summary-exporter /kube-summary-exporter ENTRYPOINT [ "/kube-summary-exporter"] diff --git a/README.md b/README.md index b3a39e7..f1bb9f6 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,16 @@ # kube-summary-exporter - -[![Build Status](https://drone.prod.merit.uw.systems/api/badges/utilitywarehouse/kube-summary-exporter/status.svg)](https://drone.prod.merit.uw.systems/utilitywarehouse/kube-summary-exporter) - Exports prometheus metrics for the Kubernetes Summary API. -## Usage +## run locally -Visiting http://localhost:9779/node/example-node will return metrics for the -node 'example-node'. +To run exporter locally run `go run ./...` -Here's an example scrape config. This assumes that the exporter is available at `kube-summary-exporter:9779`. +This will run server on default port `9779` -``` - - job_name: "kubernetes-summary" - kubernetes_sd_configs: - - role: node - relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /node/${1} - - target_label: __address__ - replacement: kube-summary-exporter:9779 -``` +Visiting http://localhost:9779/node/example-node will return metrics for the +node 'example-node'. App will look for `example-node` in the `current-context` cluster set in kube config. +[Here's an example scrape config.](manifests/scrap-config.yaml) ## Metrics | Metric | Description | Labels | diff --git a/go.mod b/go.mod index 16a2bc1..d4e375d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module github.com/utilitywarehouse/kube-summary-exporter -go 1.17 +go 1.19 require ( + github.com/google/go-cmp v0.5.5 github.com/gorilla/mux v1.8.0 github.com/prometheus/client_golang v1.11.0 k8s.io/client-go v0.22.1 @@ -24,7 +25,6 @@ require ( github.com/go-logr/logr v0.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-cmp v0.5.5 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/imdario/mergo v0.3.5 // indirect diff --git a/go.sum b/go.sum index 0718b04..522ee1e 100644 --- a/go.sum +++ b/go.sum @@ -64,7 +64,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 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= diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d1483c4 --- /dev/null +++ b/main_test.go @@ -0,0 +1,104 @@ +package main + +import ( + "encoding/json" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/prometheus/client_golang/prometheus" + _ "k8s.io/client-go/plugin/pkg/client/auth" + stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1" +) + +func Test_collectSummaryMetrics(t *testing.T) { + expectedOut := `# HELP kube_summary_container_logs_available_bytes Number of bytes that aren't consumed by the container logs +# TYPE kube_summary_container_logs_available_bytes gauge +kube_summary_container_logs_available_bytes{name="dev-server",namespace="mon",pod="dev-server-0"} 9.0016837632e+10 +# HELP kube_summary_container_logs_capacity_bytes Number of bytes that can be consumed by the container logs +# TYPE kube_summary_container_logs_capacity_bytes gauge +kube_summary_container_logs_capacity_bytes{name="dev-server",namespace="mon",pod="dev-server-0"} 1.01535985664e+11 +# HELP kube_summary_container_logs_inodes Number of Inodes for logs +# TYPE kube_summary_container_logs_inodes gauge +kube_summary_container_logs_inodes{name="dev-server",namespace="mon",pod="dev-server-0"} 2.5474432e+07 +# HELP kube_summary_container_logs_inodes_free Number of available Inodes for logs +# TYPE kube_summary_container_logs_inodes_free gauge +kube_summary_container_logs_inodes_free{name="dev-server",namespace="mon",pod="dev-server-0"} 2.5355212e+07 +# HELP kube_summary_container_logs_inodes_used Number of used Inodes for logs +# TYPE kube_summary_container_logs_inodes_used gauge +kube_summary_container_logs_inodes_used{name="dev-server",namespace="mon",pod="dev-server-0"} 1 +# HELP kube_summary_container_logs_used_bytes Number of bytes that are consumed by the container logs +# TYPE kube_summary_container_logs_used_bytes gauge +kube_summary_container_logs_used_bytes{name="dev-server",namespace="mon",pod="dev-server-0"} 8192 +# HELP kube_summary_container_rootfs_available_bytes Number of bytes that aren't consumed by the container +# TYPE kube_summary_container_rootfs_available_bytes gauge +kube_summary_container_rootfs_available_bytes{name="dev-server",namespace="mon",pod="dev-server-0"} 9.0016837632e+10 +# HELP kube_summary_container_rootfs_capacity_bytes Number of bytes that can be consumed by the container +# TYPE kube_summary_container_rootfs_capacity_bytes gauge +kube_summary_container_rootfs_capacity_bytes{name="dev-server",namespace="mon",pod="dev-server-0"} 1.01535985664e+11 +# HELP kube_summary_container_rootfs_inodes Number of Inodes +# TYPE kube_summary_container_rootfs_inodes gauge +kube_summary_container_rootfs_inodes{name="dev-server",namespace="mon",pod="dev-server-0"} 2.5474432e+07 +# HELP kube_summary_container_rootfs_inodes_free Number of available Inodes +# TYPE kube_summary_container_rootfs_inodes_free gauge +kube_summary_container_rootfs_inodes_free{name="dev-server",namespace="mon",pod="dev-server-0"} 2.5355212e+07 +# HELP kube_summary_container_rootfs_inodes_used Number of used Inodes +# TYPE kube_summary_container_rootfs_inodes_used gauge +kube_summary_container_rootfs_inodes_used{name="dev-server",namespace="mon",pod="dev-server-0"} 14 +# HELP kube_summary_container_rootfs_used_bytes Number of bytes that are consumed by the container +# TYPE kube_summary_container_rootfs_used_bytes gauge +kube_summary_container_rootfs_used_bytes{name="dev-server",namespace="mon",pod="dev-server-0"} 114688 +# HELP kube_summary_pod_ephemeral_storage_available_bytes Number of bytes of Ephemeral storage that aren't consumed by the pod +# TYPE kube_summary_pod_ephemeral_storage_available_bytes gauge +kube_summary_pod_ephemeral_storage_available_bytes{namespace="mon",pod="dev-server-0"} 9.0016837632e+10 +# HELP kube_summary_pod_ephemeral_storage_capacity_bytes Number of bytes of Ephemeral storage that can be consumed by the pod +# TYPE kube_summary_pod_ephemeral_storage_capacity_bytes gauge +kube_summary_pod_ephemeral_storage_capacity_bytes{namespace="mon",pod="dev-server-0"} 1.01535985664e+11 +# HELP kube_summary_pod_ephemeral_storage_inodes Number of Inodes for pod Ephemeral storage +# TYPE kube_summary_pod_ephemeral_storage_inodes gauge +kube_summary_pod_ephemeral_storage_inodes{namespace="mon",pod="dev-server-0"} 2.5474432e+07 +# HELP kube_summary_pod_ephemeral_storage_inodes_free Number of available Inodes for pod Ephemeral storage +# TYPE kube_summary_pod_ephemeral_storage_inodes_free gauge +kube_summary_pod_ephemeral_storage_inodes_free{namespace="mon",pod="dev-server-0"} 2.5355212e+07 +# HELP kube_summary_pod_ephemeral_storage_inodes_used Number of used Inodes for pod Ephemeral storage +# TYPE kube_summary_pod_ephemeral_storage_inodes_used gauge +kube_summary_pod_ephemeral_storage_inodes_used{namespace="mon",pod="dev-server-0"} 63 +# HELP kube_summary_pod_ephemeral_storage_used_bytes Number of bytes of Ephemeral storage that are consumed by the pod +# TYPE kube_summary_pod_ephemeral_storage_used_bytes gauge +kube_summary_pod_ephemeral_storage_used_bytes{namespace="mon",pod="dev-server-0"} 1.33947392e+08 +` + + d, err := os.ReadFile("test-summary.json") + if err != nil { + t.Fatal(err) + } + + var summary stats.Summary + registry := prometheus.NewRegistry() + + err = json.Unmarshal(d, &summary) + if err != nil { + t.Fatal(err) + } + + collectSummaryMetrics(&summary, registry) + + tmpfile, err := os.CreateTemp("", "test-summary.prom") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if err := prometheus.WriteToTextfile(tmpfile.Name(), registry); err != nil { + t.Fatal(err) + } + + fileBytes, err := os.ReadFile(tmpfile.Name()) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(string(fileBytes), expectedOut); diff != "" { + t.Errorf("collectSummaryMetrics() metrics mismatch (-want +got):\n%s", diff) + } +} diff --git a/manifests/base/deployment.yaml b/manifests/base/deployment.yaml new file mode 100644 index 0000000..943ba40 --- /dev/null +++ b/manifests/base/deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kube-summary-exporter +--- +apiVersion: v1 +kind: Service +metadata: + labels: + name: kube-summary-exporter + name: kube-summary-exporter +spec: + ports: + - name: kube-summary-exporter + protocol: TCP + port: 9779 + targetPort: 9779 + selector: + app: kube-summary-exporter +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kube-summary-exporter +spec: + replicas: 1 + selector: + matchLabels: + app: kube-summary-exporter + template: + metadata: + name: kube-summary-exporter + labels: + app: kube-summary-exporter + spec: + serviceAccountName: kube-summary-exporter + containers: + - name: kube-summary-exporter + image: quay.io/utilitywarehouse/kube-summary-exporter:latest + ports: + - name: tcp + containerPort: 9779 diff --git a/manifests/base/kustomization.yaml b/manifests/base/kustomization.yaml new file mode 100644 index 0000000..9c2d28b --- /dev/null +++ b/manifests/base/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment.yaml diff --git a/manifests/cluster/clusterrole.yaml b/manifests/cluster/clusterrole.yaml new file mode 100644 index 0000000..d8d3274 --- /dev/null +++ b/manifests/cluster/clusterrole.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kube-summary-exporter +rules: + - apiGroups: [""] + resources: ["nodes/proxy"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kube-summary-exporter +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kube-summary-exporter +subjects: + - kind: ServiceAccount + name: kube-summary-exporter + namespace: sys-prom diff --git a/manifests/cluster/kustomization.yaml b/manifests/cluster/kustomization.yaml new file mode 100644 index 0000000..b9dcee1 --- /dev/null +++ b/manifests/cluster/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - clusterrole.yaml diff --git a/manifests/scrap-config.yaml b/manifests/scrap-config.yaml new file mode 100644 index 0000000..1fd141c --- /dev/null +++ b/manifests/scrap-config.yaml @@ -0,0 +1,14 @@ +# Scrape config for the Summary API exporter. +# This assumes that the exporter is available at kube-summary-exporter:9779 +- job_name: "kubernetes-summary" + kubernetes_sd_configs: + - role: node + relabel_configs: + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - source_labels: [__meta_kubernetes_node_name] + regex: (.+) + target_label: __metrics_path__ + replacement: /node/${1} + - target_label: __address__ + replacement: kube-summary-exporter:9779 diff --git a/test-summary.json b/test-summary.json new file mode 100644 index 0000000..3ecc730 --- /dev/null +++ b/test-summary.json @@ -0,0 +1,122 @@ +{ + "pods": [ + { + "podRef": { + "name": "dev-server-0", + "namespace": "mon", + "uid": "93b0b04d-1ebe-4ad0-ada0-c29172c1ab9c" + }, + "startTime": "2022-11-30T11:42:08Z", + "containers": [ + { + "name": "dev-server", + "startTime": "2022-11-30T11:42:10Z", + "cpu": { + "time": "2022-11-30T14:14:40Z", + "usageNanoCores": 268106954, + "usageCoreNanoSeconds": 6650306864000 + }, + "memory": { + "time": "2022-11-30T14:14:40Z", + "availableBytes": 552603648, + "usageBytes": 539209728, + "workingSetBytes": 495972352, + "rssBytes": 118317056, + "pageFaults": 14962868, + "majorPageFaults": 3500 + }, + "rootfs": { + "time": "2022-11-30T14:14:37Z", + "availableBytes": 90016837632, + "capacityBytes": 101535985664, + "usedBytes": 114688, + "inodesFree": 25355212, + "inodes": 25474432, + "inodesUsed": 14 + }, + "logs": { + "time": "2022-11-30T14:14:41Z", + "availableBytes": 90016837632, + "capacityBytes": 101535985664, + "usedBytes": 8192, + "inodesFree": 25355212, + "inodes": 25474432, + "inodesUsed": 1 + } + } + ], + "cpu": { + "time": "2022-11-30T14:14:27Z", + "usageNanoCores": 390813248, + "usageCoreNanoSeconds": 8446621581000 + }, + "memory": { + "time": "2022-11-30T14:14:27Z", + "availableBytes": 1310638080, + "usageBytes": 835383296, + "workingSetBytes": 786513920, + "rssBytes": 241803264, + "pageFaults": 23482811, + "majorPageFaults": 7232 + }, + "network": { + "time": "2022-11-30T14:14:40Z", + "name": "eth0", + "rxBytes": 14522059300, + "rxErrors": 0, + "txBytes": 14549131546, + "txErrors": 0, + "interfaces": [ + { + "name": "tunl0", + "rxBytes": 0, + "rxErrors": 0, + "txBytes": 0, + "txErrors": 0 + }, + { + "name": "eth0", + "rxBytes": 14522059300, + "rxErrors": 0, + "txBytes": 14549131546, + "txErrors": 0 + } + ] + }, + "volume": [ + { + "time": "2022-11-30T14:12:48Z", + "availableBytes": 90016899072, + "capacityBytes": 101535985664, + "usedBytes": 12288, + "inodesFree": 25355211, + "inodes": 25474432, + "inodesUsed": 2, + "name": "plugins" + }, + { + "time": "2022-11-30T14:12:48Z", + "availableBytes": 90016899072, + "capacityBytes": 101535985664, + "usedBytes": 133500928, + "inodesFree": 25355211, + "inodes": 25474432, + "inodesUsed": 2, + "name": "var-files" + } + ], + "ephemeral-storage": { + "time": "2022-11-30T14:14:41Z", + "availableBytes": 90016837632, + "capacityBytes": 101535985664, + "usedBytes": 133947392, + "inodesFree": 25355212, + "inodes": 25474432, + "inodesUsed": 63 + }, + "process_stats": { + "process_count": 0 + } + } + ] +}