Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions .github/workflows/build-test-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ jobs:
name: build
strategy:
matrix:
go-version: [1.22.x]
go-version: [1.25.x]
goarch: [amd64,arm64,ppc64le,s390x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go matrix
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}

Expand Down Expand Up @@ -57,9 +57,9 @@ jobs:
name: test
steps:
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.22.x
go-version: 1.25.x

- name: Check out code into the Go module directory
uses: actions/checkout@v6
Expand All @@ -76,9 +76,9 @@ jobs:
name: test-coverage
steps:
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.22.x
go-version: 1.25.x

- uses: actions/checkout@v6

Expand All @@ -93,15 +93,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.22.x
go-version: 1.25.x
- uses: actions/checkout@v6
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v8
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.63.4
version: v2.7.2

hadolint:
runs-on: ubuntu-latest
Expand All @@ -120,9 +119,9 @@ jobs:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.22.x
go-version: 1.25.x

# if this fails, run go mod tidy
- name: Check if module files are consistent with code
Expand Down
100 changes: 100 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
version: "2"

run:
timeout: 5m

formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/k8snetworkplumbingwg/sriov-network-metrics-exporter

linters:
default: none
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- copyloopvar
- exhaustive
- funlen
- goconst
- gocritic
- gocyclo
- mnd
- goprintffuncname
- gosec
- govet
- ineffassign
- lll
- misspell
- nakedret
- prealloc
- rowserrcheck
- staticcheck
- unconvert
- unparam
- unused
- whitespace
settings:
dupl:
threshold: 100
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- dupImport
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
- unnamedResult
settings:
hugeParam:
sizeThreshold: 512
rangeValCopy:
sizeThreshold: 512
gocyclo:
min-complexity: 15
lll:
line-length: 140
misspell:
locale: US
mnd:
checks:
- argument
- case
- condition
- return
prealloc:
simple: true
range-loops: true
for-loops: false
exclusions:
rules:
- path: ".*_test\\.go"
linters:
- dupl
- funlen
- goconst
- mnd
- errcheck
- lll
- gocritic
- staticcheck
- unconvert

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/golang:alpine as builder
FROM docker.io/golang:1.25-alpine as builder

Check warning on line 1 in Dockerfile

View workflow job for this annotation

GitHub Actions / build-image

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/

RUN apk add --no-cache --virtual build-dependencies build-base linux-headers git
COPY ./ /usr/src/sriov-network-metrics-exporter
Expand Down
19 changes: 11 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,16 @@ test-coverage:
go test ./... -coverprofile cover.out
go tool cover -func cover.out

go-lint-install:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4
GOLANGCI_LINT_VER = v2.7.2
GOLANGCI_LINT = $(GOPATH)/bin/golangci-lint

go-lint: go-lint-install
go mod tidy
go fmt ./...
golangci-lint run --color always -v ./...
$(GOLANGCI_LINT): ; $(info installing golangci-lint...)
$Q go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VER)

go-lint-report: go-lint-install
golangci-lint run --color always -v ./... &> golangci-lint.txt
lint: | $(GOLANGCI_LINT) ; $(info running golangci-lint...) @ ## Run golangci-lint
$Q $(GOLANGCI_LINT) run --timeout=5m

go-lint: lint

go-lint-report: | $(GOLANGCI_LINT)
$(GOLANGCI_LINT) run --color always -v ./... &> golangci-lint.txt
19 changes: 15 additions & 4 deletions cmd/sriov-network-metrics-exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ import (
"flag"
"log"
"net/http"

"github.com/k8snetworkplumbingwg/sriov-network-metrics-exporter/collectors"
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/time/rate"

"github.com/k8snetworkplumbingwg/sriov-network-metrics-exporter/collectors"
)

const (
defaultRateBurst = 10
defaultReadHeaderTimeout = 10 * time.Second
)

var (
addr = flag.String("web.listen-address", ":9808", "Port to listen on for web interface and telemetry.")
rateLimit = flag.Int("web.rate-limit", 1, "Limit for requests per second.")
rateBurst = flag.Int("web.rate-burst", 10, "Maximum per second burst rate for requests.")
rateBurst = flag.Int("web.rate-burst", defaultRateBurst, "Maximum per second burst rate for requests.")
metricsEndpoint = "/metrics"
)

Expand All @@ -38,8 +44,13 @@ func main() {
noBody(promhttp.Handler()), metricsEndpoint)),
rate.Limit(*rateLimit), *rateBurst)

server := &http.Server{
Addr: *addr,
Handler: handlerWithMiddleware,
ReadHeaderTimeout: defaultReadHeaderTimeout,
}
log.Printf("listening on %v", *addr)
log.Fatalf("ListenAndServe error: %v", http.ListenAndServe(*addr, handlerWithMiddleware))
log.Fatalf("ListenAndServe error: %v", server.ListenAndServe())
}

func parseAndVerifyFlags() {
Expand Down
6 changes: 3 additions & 3 deletions cmd/sriov-network-metrics-exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestMain(t *testing.T) {
var _ = DescribeTable("test endpointOnly handler", // endpointOnly
func(endpoint string, expectedResponse int) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, endpoint, nil)
request := httptest.NewRequest(http.MethodGet, endpoint, http.NoBody)
handler := endpointOnly(promhttp.Handler(), metricsEndpoint)

handler.ServeHTTP(recorder, request)
Expand All @@ -35,7 +35,7 @@ var _ = DescribeTable("test endpointOnly handler", // endpointOnly
var _ = DescribeTable("test getOnly handler", // getOnly
func(method string, expectedResponse int) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(method, metricsEndpoint, nil)
request := httptest.NewRequest(method, metricsEndpoint, http.NoBody)
handler := getOnly(promhttp.Handler())

handler.ServeHTTP(recorder, request)
Expand Down Expand Up @@ -67,7 +67,7 @@ var _ = DescribeTable("test limitRequests handler", // limitRequests
code := http.StatusOK
for i := 0; i < requests; i++ {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, metricsEndpoint, nil)
request := httptest.NewRequest(http.MethodGet, metricsEndpoint, http.NoBody)
handler.ServeHTTP(recorder, request)

code = recorder.Code
Expand Down
9 changes: 6 additions & 3 deletions collectors/collectors.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Package Collectors defines the structure of the collector aggregator and contains the individual collectors used to gather metrics
// Each collector should be created in its own file with any required command line flags, its collection behavior and its registration method defined.
// Package collectors defines the structure of the collector aggregator and contains the individual collectors
// used to gather metrics.
// Each collector should be created in its own file with any required command line flags, its collection
// behavior and its registration method defined.

package collectors

Expand All @@ -22,7 +24,8 @@ var (
// SriovCollector registers the collectors used for specific data and exposes a Collect method to gather the data
type SriovCollector []prometheus.Collector

// Register defines a flag for a collector and adds it to the registry of enabled collectors if the flag is set to true - either through the default option or the flag passed on start
// Register defines a flag for a collector and adds it to the registry of enabled collectors
// if the flag is set to true - either through the default option or the flag passed on start.
// Run by each individual collector in its init function.
func register(name string, enabled bool, collector func() prometheus.Collector) {
collectorState[name] = &enabled
Expand Down
30 changes: 22 additions & 8 deletions collectors/collectors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"path/filepath"
"testing"
"testing/fstest"
"time"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -81,21 +82,34 @@ var _ = DescribeTable("test registering collector", // register

func assertLogs(logs []string) {
for _, log := range logs {
Eventually(&buffer).WithTimeout(time.Duration(2 * time.Second)).Should(gbytes.Say(log))
Eventually(&buffer).WithTimeout(2 * time.Second).Should(gbytes.Say(log))
}
}

// Replaces filepath.EvalSymlinks with an emulated evaluation to work with the in-memory fs.
var evalSymlinks = func(path string) (string, error) {
path = filepath.Join(filepath.Base(filepath.Dir(path)), filepath.Base(path))
dir := filepath.Dir(path)
base := filepath.Base(path)

if stat, err := fs.Stat(devfs, path); err == nil && stat.Mode() == fs.ModeSymlink {
if target, err := fs.ReadFile(devfs, path); err == nil {
return string(target), nil
} else {
return "", fmt.Errorf("error")
entries, err := fs.ReadDir(devfs, dir)
if err != nil {
return "", fmt.Errorf("error reading dir: %v", err)
}

for _, entry := range entries {
if entry.Name() == base && entry.Type()&fs.ModeSymlink != 0 {
// In Go 1.25, fstest.MapFS treats fs.ModeSymlink files as actual symlinks
// and tries to follow them, so fs.ReadFile won't work.
// Access the MapFS directly to get the symlink target data.
if mapFS, ok := devfs.(fstest.MapFS); ok {
if mapFile, exists := mapFS[path]; exists {
return string(mapFile.Data), nil
}
}
return "", fmt.Errorf("error reading symlink target")
}
} else {
return "", fmt.Errorf("error")
}

return "", fmt.Errorf("not a symlink or not found")
}
16 changes: 10 additions & 6 deletions collectors/pod_cpu_link.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package collectors

// kubepodCPUCollector is a Kubernetes focused collector that exposes information about CPUs linked to specific Kubernetes pods through the CPU Manager component in Kubelet
// kubepodCPUCollector is a Kubernetes focused collector that exposes information about CPUs
// linked to specific Kubernetes pods through the CPU Manager component in Kubelet

import (
"encoding/json"
Expand All @@ -21,9 +22,11 @@ import (

var (
kubepodcpu = "kubepodcpu"
kubePodCgroupPath = flag.String("path.kubecgroup", "/sys/fs/cgroup/cpuset/kubepods.slice/", "Path for location of kubernetes cgroups on the host system")
kubePodCgroupPath = flag.String("path.kubecgroup",
"/sys/fs/cgroup/cpuset/kubepods.slice/", "Path for kubernetes cgroups")
sysDevSysNodePath = flag.String("path.nodecpuinfo", "/sys/devices/system/node/", "Path for location of system cpu information")
cpuCheckPointFile = flag.String("path.cpucheckpoint", "/var/lib/kubelet/cpu_manager_state", "Path for location of cpu manager checkpoint file")
cpuCheckPointFile = flag.String("path.cpucheckpoint",
"/var/lib/kubelet/cpu_manager_state", "Path for cpu manager checkpoint file")

kubecgroupfs fs.FS
cpuinfofs fs.FS
Expand Down Expand Up @@ -108,7 +111,7 @@ func (c kubepodCPUCollector) Describe(ch chan<- *prometheus.Desc) {
func createKubepodCPUCollector() prometheus.Collector {
cpuInfo, err := getCPUInfo()
if err != nil {
//Exporter will fail here if file can not be read.
// Exporter will fail here if file can not be read.
logFatal("Fatal Error: cpu info for node can not be collected, %v", err.Error())
}

Expand Down Expand Up @@ -154,7 +157,8 @@ func getCPUInfo() (map[string]string, error) {
// This accounting will create an entry for each guaranteed pod, even if that pod isn't managed by CPU manager
// i.e. it will still create an entry if the pod is looking for millis of CPU
// Todo: validate regex matching and evaluate performance of this approach
// Todo: validate assumptions about directory structure against other runtimes and kubelet config. Plausibly problematic with CgroupsPerQos and other possible future cgroup changes
// Todo: validate assumptions about directory structure against other runtimes and kubelet config.
// Plausibly problematic with CgroupsPerQos and other possible future cgroup changes
func getGuaranteedPodCPUs() ([]podCPULink, error) {
links := make([]podCPULink, 0)

Expand Down Expand Up @@ -259,7 +263,7 @@ func parseCPURange(cpuString string) ([]string, error) {
endpoints := strings.Split(r, "-")
if len(endpoints) == 1 {
cpuList = append(cpuList, endpoints[0])
} else if len(endpoints) == 2 {
} else if len(endpoints) == 2 { //nolint:mnd
start, err := strconv.Atoi(endpoints[0])
if err != nil {
return cpuList, err
Expand Down
Loading
Loading