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
7 changes: 5 additions & 2 deletions kubelet-to-gcm/monitor/config/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@ package config

import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"

"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor"
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
)

const (
gceMetaDataEndpoint = "http://169.254.169.254"
gceMetaDataPrefix = "/computeMetadata/v1"
// maxResponseBodySize is the maximum size of the response body we will read.
// 10MB is more than enough for GCE metadata responses.
maxResponseBodySize = 10 * 1024 * 1024
)

// NewConfigs returns the SourceConfigs for all monitored endpoints, and
Expand Down Expand Up @@ -117,7 +120,7 @@ func getGCEMetaData(uri string) ([]byte, error) {
return nil, fmt.Errorf("Failed request %q for GCE metadata: %v", uri, err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body, err := util.ReadWithLimit(resp.Body, maxResponseBodySize)
if err != nil {
return nil, fmt.Errorf("Failed to read body for request %q for GCE metadata: %v", uri, err)
}
Expand Down
10 changes: 8 additions & 2 deletions kubelet-to-gcm/monitor/controller/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ package controller
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"

"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/common/model"
)
Expand Down Expand Up @@ -72,6 +72,12 @@ func NewMetrics(body []byte) (*Metrics, error) {
return metrics, nil
}

const (
// maxResponseBodySize is the maximum size of the response body we will read.
// 10MB is more than enough for most prometheus metrics responses.
maxResponseBodySize = 10 * 1024 * 1024
)

// Client queries metrics from the controller process.
type Client struct {
client *http.Client
Expand Down Expand Up @@ -99,7 +105,7 @@ func (c *Client) doRequestAndParse(req *http.Request) (*Metrics, error) {
return nil, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
body, err := util.ReadWithLimit(response.Body, maxResponseBodySize)
if err != nil {
return nil, fmt.Errorf("failed to read response body - %v", err)
}
Expand Down
90 changes: 90 additions & 0 deletions kubelet-to-gcm/monitor/controller/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright 2017 Google 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.
*/

package controller

import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
)

func TestDoRequestAndParse_SizeLimit(t *testing.T) {
// Create a mock server that returns a response larger than maxResponseBodySize.
largeDataSize := maxResponseBodySize + 1024
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Write more than maxResponseBodySize bytes.
data := make([]byte, largeDataSize)
w.Write(data)
}))
defer ts.Close()

u, _ := url.Parse(ts.URL)
client := &Client{
client: ts.Client(),
metricsURL: u,
}

req, _ := http.NewRequest("GET", ts.URL, nil)
_, err := client.doRequestAndParse(req)

if err == nil {
t.Fatal("Expected error due to size limit, but got none")
}

if !strings.Contains(err.Error(), util.ErrBodyTooLarge.Error()) {
t.Errorf("Expected error containing %q, got %v", util.ErrBodyTooLarge, err)
}
}

func TestDoRequestAndParse_Success(t *testing.T) {
metricsData := `
# HELP node_collector_evictions_number Number of evictions
# TYPE node_collector_evictions_number counter
node_collector_evictions_number 10
# HELP process_start_time_seconds Start time
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1234567890
`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(metricsData))
}))
defer ts.Close()

u, _ := url.Parse(ts.URL)
client := &Client{
client: ts.Client(),
metricsURL: u,
}

req, _ := http.NewRequest("GET", ts.URL, nil)
metrics, err := client.doRequestAndParse(req)

if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

if metrics.NodeEvictions != 10 {
t.Errorf("Expected 10 evictions, got %d", metrics.NodeEvictions)
}
if metrics.CreateTime != 1234567890 {
t.Errorf("Expected 1234567890 create time, got %d", metrics.CreateTime)
}
}
10 changes: 8 additions & 2 deletions kubelet-to-gcm/monitor/kubelet/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ package kubelet
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"

"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)

const (
// maxResponseBodySize is the maximum size of the response body we will read.
// 50MB should be enough for any Kubelet stats summary response.
maxResponseBodySize = 50 * 1024 * 1024
)

// Client contains all the information and methods to encapsulate
// communication with the Kubelet.
type Client struct {
Expand Down Expand Up @@ -58,7 +64,7 @@ func (k *Client) doRequestAndUnmarshal(client *http.Client, req *http.Request, v
return err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
body, err := util.ReadWithLimit(response.Body, maxResponseBodySize)
if err != nil {
return fmt.Errorf("failed to read response body - %v", err)
}
Expand Down
88 changes: 88 additions & 0 deletions kubelet-to-gcm/monitor/kubelet/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2017 Google 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.
*/

package kubelet

import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
)

func TestDoRequestAndUnmarshal_SizeLimit(t *testing.T) {
largeDataSize := maxResponseBodySize + 1024
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data := make([]byte, largeDataSize)
w.Write(data)
}))
defer ts.Close()

u, _ := url.Parse(ts.URL)
k := &Client{
client: ts.Client(),
summaryURL: u,
}

req, _ := http.NewRequest("GET", ts.URL, nil)
var value stats.Summary
err := k.doRequestAndUnmarshal(ts.Client(), req, &value)

if err == nil {
t.Fatal("Expected error due to size limit, but got none")
}

if !strings.Contains(err.Error(), util.ErrBodyTooLarge.Error()) {
t.Errorf("Expected error containing %q, got %v", util.ErrBodyTooLarge, err)
}
}

func TestDoRequestAndUnmarshal_Success(t *testing.T) {
summary := &stats.Summary{
Node: stats.NodeStats{
NodeName: "test-node",
},
}
data, _ := json.Marshal(summary)

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(data)
}))
defer ts.Close()

u, _ := url.Parse(ts.URL)
k := &Client{
client: ts.Client(),
summaryURL: u,
}

req, _ := http.NewRequest("GET", ts.URL, nil)
var result stats.Summary
err := k.doRequestAndUnmarshal(ts.Client(), req, &result)

if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

if result.Node.NodeName != "test-node" {
t.Errorf("Expected test-node, got %s", result.Node.NodeName)
}
}
41 changes: 41 additions & 0 deletions kubelet-to-gcm/monitor/util/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright 2026 Google 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.
*/

package util

import (
"errors"
"io"
)

var (
// ErrBodyTooLarge is returned when the response body exceeds the configured limit.
ErrBodyTooLarge = errors.New("response body too large")
)

// ReadWithLimit reads from r until EOF or the limit is reached.
// If the limit is exceeded, it returns ErrBodyTooLarge.
func ReadWithLimit(r io.Reader, limit int64) ([]byte, error) {
// Read up to limit + 1 bytes to detect overflow.
data, err := io.ReadAll(io.LimitReader(r, limit+1))
if err != nil {
return nil, err
}
if int64(len(data)) > limit {
return nil, ErrBodyTooLarge
}
return data, nil
}
67 changes: 67 additions & 0 deletions kubelet-to-gcm/monitor/util/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright 2026 Google 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.
*/

package util

import (
"bytes"
"strings"
"testing"
)

func TestReadWithLimit(t *testing.T) {
testCases := []struct {
name string
input string
limit int64
expected string
expectedErr error
}{
{
name: "under limit",
input: "short",
limit: 10,
expected: "short",
expectedErr: nil,
},
{
name: "at limit",
input: "exactlimit",
limit: 10,
expected: "exactlimit",
expectedErr: nil,
},
{
name: "over limit",
input: "this is way too long",
limit: 10,
expected: "",
expectedErr: ErrBodyTooLarge,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := ReadWithLimit(strings.NewReader(tc.input), tc.limit)
if err != tc.expectedErr {
t.Fatalf("Expected error %v, got %v", tc.expectedErr, err)
}
if tc.expectedErr == nil && !bytes.Equal(got, []byte(tc.expected)) {
t.Errorf("Expected data %q, got %q", tc.expected, string(got))
}
})
}
}
Loading