Testing and Debugging for Kubernetes Client-Go
Thorough testing and debugging are crucial to ensure the reliability and correctness of the client library and applications that utilize it. Here are some common approaches for testing and debugging client-go code:
1. Fake Client:
- Use a fake client in tests to simulate the behavior of the Kubernetes API server without interacting with a real cluster.
- This approach helps isolate the code under test and improve the speed and reliability of your test suite.
Example:
The examples/fake-client
directory demonstrates how to use a fake client with SharedInformerFactory:
go test -v k8s.io/client-go/examples/fake-client
2. Debugging Round Tripper:
- This round tripper enables debugging of certain HTTP requests and responses fields, providing insights into the communication with the Kubernetes API server.
DebugJustURL
,DebugURLTiming
,DebugCurlCommand
,DebugRequestHeaders
,DebugResponseStatus
,DebugResponseHeaders
, andDebugDetailedTiming
provide different levels of debugging information.
Example:
// transport/round_trippers.go
const (
// DebugJustURL will add to the debug output HTTP requests method and url.
DebugJustURL DebugLevel = iota
// DebugURLTiming will add to the debug output the duration of HTTP requests.
DebugURLTiming
// DebugCurlCommand will add to the debug output the curl command equivalent to the
// HTTP request.
DebugCurlCommand
// DebugRequestHeaders will add to the debug output the HTTP requests headers.
DebugRequestHeaders
// DebugResponseStatus will add to the debug output the HTTP response status.
DebugResponseStatus
// DebugResponseHeaders will add to the debug output the HTTP response headers.
DebugResponseHeaders
// DebugDetailedTiming will add to the debug output the duration of the HTTP requests events.
DebugDetailedTiming
)
// DebugLevel is used to enable debugging of certain
// HTTP requests and responses fields via the debuggingRoundTripper.
type DebugLevel int
3. Unit Tests:
- Utilize unit tests to test individual functions and components of the client library in isolation.
- This approach allows you to pinpoint and fix bugs quickly.
Example:
// dynamic/fake/simple_test.go
// verifyErr verifies that the given error returned from Patch is the error
// expected by the test case.
func (tc *patchTestCase) verifyErr(err error) error {
if tc.wantErrMsg != "" && err == nil {
return fmt.Errorf("want error, got nil")
}
if tc.wantErrMsg == "" && err != nil {
return fmt.Errorf("want no error, got %v", err)
}
if err != nil {
if want, got := tc.wantErrMsg, err.Error(); want != got {
return fmt.Errorf("incorrect error: want: %q got: %q", want, got)
}
}
return nil
}
func (tc *patchTestCase) verifyResult(result *unstructured.Unstructured) error {
if tc.expectedPatchedObject == nil && result == nil {
return nil
}
if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) {
return fmt.Errorf("unexpected diff in received object: %s", cmp.Diff(tc.expectedPatchedObject, result))
}
return nil
}
4. Integration Tests:
- Integration tests validate the interactions between different components of the client library and the Kubernetes API server.
- This approach ensures that the client library works as expected in a real-world scenario.
Example:
// features/testing/features_init_test.go
func assertFunctionPanicsWithMessage(t *testing.T, f func(), fName, errMessage string) {
didPanic, panicMessage := didFunctionPanic(f)
if !didPanic {
t.Fatalf("function %q did not panicked", fName)
}
panicError, ok := panicMessage.(error)
if !ok || !strings.Contains(panicError.Error(), errMessage) {
t.Fatalf("func %q should panic with error message:\t%#v\n\tPanic value:\t%#v\n", fName, errMessage, panicMessage)
}
}
func TestOverridesForSetFeatureGatesDuringTest(t *testing.T) {
scenarios := []struct {
name string
firstTestName string
secondTestName string
expectError bool
}{
{
name: "concurrent tests setting the same feature fail",
firstTestName: "fooTest",
secondTestName: "barTest",
expectError: true,
},
{
name: "same test setting the same feature does not fail",
firstTestName: "fooTest",
secondTestName: "fooTest",
expectError: false,
},
{
name: "subtests setting the same feature don't not fail",
firstTestName: "fooTest",
secondTestName: "fooTest/scenario1",
expectError: false,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
featureA := features.Feature("FeatureA")
fakeGates := &fakeFeatureGates{map[features.Feature]bool{featureA: true}}
fakeTesting := &fakeT{fakeTestName: scenario.firstTestName, TB: t}
features.ReplaceFeatureGates(fakeGates)
require.NoError(t, setFeatureDuringTestInternal(fakeTesting, featureA, true))
require.True(t, features.FeatureGates().Enabled(featureA))
fakeTesting.fakeTestName = scenario.secondTestName
err := setFeatureDuringTestInternal(fakeTesting, featureA, false)
require.Equal(t, scenario.expectError, err != nil)
})
}
}
5. End-to-End Tests:
- End-to-end tests simulate a complete workflow using the client library, verifying its functionality from start to finish.
- This approach ensures that the client library integrates well with other components and delivers the expected functionality in a real-world scenario.
Example:
// metadata/fake/simple_test.go
// verifyErr verifies that the given error returned from Patch is the error
// expected by the test case.
func (tc *patchTestCase) verifyErr(err error) error {
if tc.wantErrMsg != "" && err == nil {
return fmt.Errorf("want error, got nil")
}
if tc.wantErrMsg == "" && err != nil {
return fmt.Errorf("want no error, got %v", err)
}
if err != nil {
if want, got := tc.wantErrMsg, err.Error(); want != got {
return fmt.Errorf("incorrect error: want: %q got: %q", want, got)
}
}
return nil
}
6. Debugging Tools:
- Utilize debugging tools like
kubectl
,go test -v
, andgo run -race
to investigate and fix issues.
Example:
// rest/request_test.go
func TestVerbs(t *testing.T) {
c := testRESTClient(t, nil)
if r := c.Post(); r.verb != "POST" {
t.Errorf("Post verb is wrong")
}
if r := c.Put(); r.verb != "PUT" {
t.Errorf("Put verb is wrong")
}
if r := c.Get(); r.verb != "GET" {
t.Errorf("Get verb is wrong")
}
if r := c.Delete(); r.verb != "DELETE" {
t.Errorf("Delete verb is wrong")
}
}
// rest/config_test.go
func TestBuildUserAgent(t *testing.T) {
assert.New(t).Equal(
"lynx/nicest (beos/itanium) kubernetes/baaaaaaaaad",
buildUserAgent(
"lynx", "nicest",
"beos", "itanium", "baaaaaaaaad"))
}
func TestAdjustCommand(t *testing.T) {
assert := assert.New(t)
assert.Equal("beans", adjustCommand(filepath.Join("home", "bob", "Downloads", "beans")))
assert.Equal("beans", adjustCommand(filepath.Join(".", "beans")))
assert.Equal("beans", adjustCommand("beans"))
assert.Equal("unknown", adjustCommand(""))
}
// tools/record/main_test.go
/*
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
7. Example Programs:
- The
examples
directory contains a variety of examples demonstrating different use cases and functionalities of client-go. - These examples serve as valuable resources for understanding how to use the library and can be adapted for your specific needs.
Example:
// util/testing/fake_handler_test.go
func TestFakeHandlerWrongMethod(t *testing.T) {
handler := FakeHandler{StatusCode: http.StatusOK}
server := httptest.NewServer(&handler)
defer server.Close()
method := "GET"
path := "/foo/bar"
fakeT := fakeError{}
req, err := http.NewRequest("PUT", server.URL+path, nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
client := http.Client{}
_, err = client.Do(req)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
handler.ValidateRequest(&fakeT, path, method, nil)
if len(fakeT.errors) != 1 {
t.Errorf("Unexpected error set: %#v", fakeT.errors)
}
}
// util/workqueue/delaying_queue_test.go
func TestAddTwoFireEarly(t *testing.T) {
fakeClock := testingclock.NewFakeClock(time.Now())
q := NewDelayingQueueWithConfig(DelayingQueueConfig{Clock: fakeClock})
first := "foo"
second := "bar"
third := "baz"
q.AddAfter(first, 1*time.Second)
q.AddAfter(second, 50*time.Millisecond)
if err := waitForWaitingQueueToFill(q); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if q.Len() != 0 {
t.Errorf("should not have added")
}
fakeClock.Step(60 * time.Millisecond)
if err := waitForAdded(q, 1); err != nil {
t.Fatalf("unexpected err: %v", err)
}
item, _ := q.Get()
if !reflect.DeepEqual(item, second) {
t.Errorf("expected %v, got %v", second, item)
}
q.AddAfter(third, 2*time.Second)
fakeClock.Step(1 * time.Second)
if err := waitForAdded(q, 1); err != nil {
t.Fatalf("unexpected err: %v", err)
}
item, _ = q.Get()
if !reflect.DeepEqual(item, first) {
t.Errorf("expected %v, got %v", first, item)
}
fakeClock.Step(2 * time.Second)
if err := waitForAdded(q, 1); err != nil {
t.Fatalf("unexpected err: %v", err)
}
item, _ = q.Get()
if !reflect.DeepEqual(item, third) {
t.Errorf("expected %v, got %v", third, item)
}
}
8. Community Support:
- Utilize the Kubernetes community for support, asking questions and sharing experiences on forums like Stack Overflow and the Kubernetes Slack.
9. Code Review:
- Engage in code reviews to ensure the quality of your code, catch potential issues early, and learn from other developers.
10. Documentation:
- Refer to the official client-go documentation for detailed information about the library’s API, features, and best practices.
- This includes the examples provided in the
examples
directory and theREADME.md
files for each example.
Example:
# Examples
This directory contains examples that cover various use cases and functionality
for client-go.
## Dynamic Client - Create, Update, Delete Deployment
This example program demonstrates the fundamental operations for managing on
[Deployment][1] resources, such as `Create`, `List`, `Update` and `Delete` using client-go's `dynamic` package.
[1]: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
Top-Level Directory Explanations
applyconfigurations/ - This directory contains examples and tests for applying Kubernetes configurations using the client-go library.
discovery/ - This directory contains code related to service discovery in Kubernetes.
dynamic/ - This directory contains code for working with dynamic resources in the Kubernetes API.
examples/ - This directory contains example usage of the client-go library.
features/ - This directory contains experimental features that are not yet stable.
kubernetes/ - This directory contains the main package for the client-go library, which provides types and functions for interacting with the Kubernetes API.
listers/ - This directory contains interfaces and implementations for listing Kubernetes resources.
metadata/ - This directory contains code related to metadata in Kubernetes.
plugin/ - This directory contains code for loading plugins for the client-go library.
rest/ - This directory contains code for working with the REST API in the client-go library.
scale/ - This directory contains code for working with scale and autoscaling in Kubernetes.
tools/ - This directory contains various tools for working with the client-go library.
util/ - This directory contains utility functions for the client-go library.