Skip to content

Commit

Permalink
Merge pull request #242 from tropnikovvl/main
Browse files Browse the repository at this point in the history
add support for K8s metrics API as a source of PVC usage data
  • Loading branch information
llamerada-jp authored Feb 14, 2024
2 parents f288c9b + 4142536 commit a73a201
Show file tree
Hide file tree
Showing 14 changed files with 311 additions and 108 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,20 @@ jobs:
- run: make -C test/e2e setup
- run: make -C test/e2e init-app-without-cert-manager
- run: make -C test/e2e test

e2e-k8s-with-metrics-api:
name: "e2e-k8s-with-metrics-api"
runs-on: "ubuntu-20.04"
strategy:
matrix:
kubernetes_versions: ["1.28.0", "1.27.3", "1.26.6"]
env:
KUBERNETES_VERSION: ${{ matrix.kubernetes_versions }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- run: make -C test/e2e setup
- run: make -C test/e2e init-app-with-metrics-api
- run: make -C test/e2e test
1 change: 1 addition & 0 deletions charts/pvc-autoresizer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ helm upgrade --create-namespace --namespace pvc-autoresizer -i pvc-autoresizer -
| controller.args.interval | string | `"10s"` | Specify interval to monitor pvc capacity. Used as "--interval" option |
| controller.args.namespaces | list | `[]` | Specify namespaces to control the pvcs of. Empty for all namespaces. Used as "--namespaces" option |
| controller.args.prometheusURL | string | `"http://prometheus-prometheus-oper-prometheus.prometheus.svc:9090"` | Specify Prometheus URL to query volume stats. Used as "--prometheus-url" option |
| controller.args.useK8sMetricsApi | bool | `false` | Use Kubernetes metrics API instead of Prometheus. Used as "--use-k8s-metrics-api" option |
| controller.nodeSelector | object | `{}` | Map of key-value pairs for scheduling pods on specific nodes. |
| controller.podAnnotations | object | `{}` | Annotations to be added to controller pods. |
| controller.podLabels | object | `{}` | Pod labels to be added to controller pods. |
Expand Down
18 changes: 18 additions & 0 deletions charts/pvc-autoresizer/templates/controller/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,21 @@ rules:
- watch
- patch
- update
{{- if .Values.controller.args.useK8sMetricsApi }}
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- "nodes/proxy"
verbs:
- get
- list
- watch
{{- end }}
3 changes: 3 additions & 0 deletions charts/pvc-autoresizer/templates/controller/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ spec:
args:
- --prometheus-url={{ .Values.controller.args.prometheusURL }}
- --interval={{ .Values.controller.args.interval }}
{{- if .Values.controller.args.useK8sMetricsApi }}
- --use-k8s-metrics-api={{ .Values.controller.args.useK8sMetricsApi }}
{{- end }}
{{- if .Values.controller.args.namespaces }}
- --namespaces={{ join "," .Values.controller.args.namespaces }}
{{- end }}
Expand Down
4 changes: 4 additions & 0 deletions charts/pvc-autoresizer/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ controller:
replicas: 1

args:
# controller.args.useK8sMetricsApi -- Use Kubernetes metrics API instead of Prometheus.
# Used as "--use-k8s-metrics-api" option
useK8sMetricsApi: false

# controller.args.prometheusURL -- Specify Prometheus URL to query volume stats.
# Used as "--prometheus-url" option
prometheusURL: http://prometheus-prometheus-oper-prometheus.prometheus.svc:9090
Expand Down
21 changes: 11 additions & 10 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import (
)

var config struct {
certDir string
webhookAddr string
metricsAddr string
healthAddr string
namespaces []string
watchInterval time.Duration
prometheusURL string
skipAnnotation bool
development bool
certDir string
webhookAddr string
metricsAddr string
healthAddr string
namespaces []string
watchInterval time.Duration
prometheusURL string
useK8sMetricsApi bool
skipAnnotation bool
development bool
}

// rootCmd represents the base command when called without any subcommands
Expand Down Expand Up @@ -51,7 +52,7 @@ func init() {
"Namespaces to resize PersistentVolumeClaims within. Empty for all namespaces.")
fs.DurationVar(&config.watchInterval, "interval", 1*time.Minute, "Interval to monitor pvc capacity.")
fs.StringVar(&config.prometheusURL, "prometheus-url", "", "Prometheus URL to query volume stats.")
fs.BoolVar(&config.useK8sMetricsApi, "use-k8s-metrics-api", false, "Use Kubernetes metrics API instead of Prometheus")
fs.BoolVar(&config.skipAnnotation, "no-annotation-check", false, "Skip annotation check for StorageClass")
fs.BoolVar(&config.development, "development", false, "Use development logger config")
_ = rootCmd.MarkFlagRequired("prometheus-url")
}
15 changes: 12 additions & 3 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,18 @@ func subMain() error {
return err
}

promClient, err := runners.NewPrometheusClient(config.prometheusURL)
var metricsClient runners.MetricsClient
if config.useK8sMetricsApi {
metricsClient, err = runners.NewK8sMetricsApiClient()
} else if config.prometheusURL != "" {
metricsClient, err = runners.NewPrometheusClient(config.prometheusURL)
} else {
setupLog.Error(err, "enable use-k8s-metrics-api or provide prometheus-url")
return err
}

if err != nil {
setupLog.Error(err, "unable to initialize prometheus client")
setupLog.Error(err, "unable to initialize metrics client")
return err
}

Expand All @@ -113,7 +122,7 @@ func subMain() error {
return err
}

pvcAutoresizer := runners.NewPVCAutoresizer(promClient, mgr.GetClient(),
pvcAutoresizer := runners.NewPVCAutoresizer(metricsClient, mgr.GetClient(),
ctrl.Log.WithName("pvc-autoresizer"),
config.watchInterval, mgr.GetEventRecorderFor("pvc-autoresizer"))
if err := mgr.Add(pvcAutoresizer); err != nil {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ require (
github.com/go-logr/logr v1.3.0
github.com/onsi/ginkgo/v2 v2.11.0
github.com/onsi/gomega v1.27.10
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.16.0
github.com/prometheus/client_model v0.4.0
github.com/prometheus/common v0.44.0
github.com/spf13/cobra v1.7.0
golang.org/x/sync v0.5.0
k8s.io/api v0.28.6
k8s.io/apimachinery v0.28.6
k8s.io/client-go v0.28.6
Expand Down Expand Up @@ -46,7 +48,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/multierr v1.11.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
127 changes: 127 additions & 0 deletions internal/runners/k8s_metrics_api_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package runners

import (
"bytes"
"context"

"github.com/pkg/errors"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"golang.org/x/sync/errgroup"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

// NewK8sMetricsApiClient returns a new k8sMetricsApiClient client
func NewK8sMetricsApiClient() (MetricsClient, error) {
return &k8sMetricsApiClient{}, nil
}

type k8sMetricsApiClient struct {
}

func (c *k8sMetricsApiClient) GetMetrics(ctx context.Context) (map[types.NamespacedName]*VolumeStats, error) {
// create a Kubernetes client using in-cluster configuration
config, err := rest.InClusterConfig()
if err != nil {
return nil, err
}

clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}

// get a list of nodes and IP addresses
nodes, err := clientset.CoreV1().Nodes().List(context.Background(), v1.ListOptions{})
if err != nil {
return nil, err
}

// create a map to hold PVC usage data
pvcUsage := make(map[types.NamespacedName]*VolumeStats)

// use an errgroup to query kubelet for PVC usage on each node
eg, ctx := errgroup.WithContext(ctx)
for _, node := range nodes.Items {
nodeName := node.Name
eg.Go(func() error {
return getPVCUsage(clientset, nodeName, pvcUsage, ctx)
})
}

// wait for all queries to complete and handle any errors
if err := eg.Wait(); err != nil {
return nil, err
}

return pvcUsage, nil
}

func getPVCUsage(clientset *kubernetes.Clientset, nodeName string, pvcUsage map[types.NamespacedName]*VolumeStats, ctx context.Context) error {
// make the request to the api /metrics endpoint and handle the response
req := clientset.
CoreV1().
RESTClient().
Get().
Resource("nodes").
Name(nodeName).
SubResource("proxy").
Suffix("metrics")
respBody, err := req.DoRaw(ctx)
if err != nil {
return errors.Errorf("failed to get stats from kubelet on node %s: with error %s", nodeName, err)
}
parser := expfmt.TextParser{}
metricFamilies, err := parser.TextToMetricFamilies(bytes.NewReader(respBody))
if err != nil {
return errors.Wrapf(err, "failed to read response body from kubelet on node %s", nodeName)
}

// volumeAvailableQuery
if gauge, ok := metricFamilies[volumeAvailableQuery]; ok {
for _, m := range gauge.Metric {
pvcName, value := parseMetric(m)
pvcUsage[pvcName] = new(VolumeStats)
pvcUsage[pvcName].AvailableBytes = int64(value)
}
}
// volumeCapacityQuery
if gauge, ok := metricFamilies[volumeCapacityQuery]; ok {
for _, m := range gauge.Metric {
pvcName, value := parseMetric(m)
pvcUsage[pvcName].CapacityBytes = int64(value)
}
}

// inodesAvailableQuery
if gauge, ok := metricFamilies[inodesAvailableQuery]; ok {
for _, m := range gauge.Metric {
pvcName, value := parseMetric(m)
pvcUsage[pvcName].AvailableInodeSize = int64(value)
}
}

// inodesCapacityQuery
if gauge, ok := metricFamilies[inodesCapacityQuery]; ok {
for _, m := range gauge.Metric {
pvcName, value := parseMetric(m)
pvcUsage[pvcName].CapacityInodeSize = int64(value)
}
}
return nil
}

func parseMetric(m *dto.Metric) (pvcName types.NamespacedName, value uint64) {
for _, label := range m.GetLabel() {
if label.GetName() == "namespace" {
pvcName.Namespace = label.GetValue()
} else if label.GetName() == "persistentvolumeclaim" {
pvcName.Name = label.GetValue()
}
}
value = uint64(m.GetGauge().GetValue())
return pvcName, value
}
Loading

0 comments on commit a73a201

Please sign in to comment.