Skip to content

Commit 31d09fc

Browse files
committed
Add ability to also get pod logs
Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
1 parent ec732e6 commit 31d09fc

File tree

4 files changed

+195
-19
lines changed

4 files changed

+195
-19
lines changed

pkg/k8s/client.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,21 @@ import (
1010
"k8s.io/apimachinery/pkg/runtime/schema"
1111
"k8s.io/client-go/discovery"
1212
"k8s.io/client-go/dynamic"
13+
"k8s.io/client-go/kubernetes"
1314
"k8s.io/client-go/rest"
1415
"k8s.io/client-go/tools/clientcmd"
1516
"k8s.io/client-go/util/homedir"
1617
)
1718

19+
// PodLogsFunc is a function type for getting pod logs
20+
type PodLogsFunc func(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error)
21+
1822
// Client represents a Kubernetes client with discovery and dynamic capabilities
1923
type Client struct {
2024
discoveryClient discovery.DiscoveryInterface
2125
dynamicClient dynamic.Interface
26+
clientset kubernetes.Interface
27+
getPodLogs PodLogsFunc
2228
}
2329

2430
// NewClient creates a new Kubernetes client
@@ -40,10 +46,22 @@ func NewClient(kubeconfigPath string) (*Client, error) {
4046
return nil, fmt.Errorf("failed to create dynamic client: %w", err)
4147
}
4248

43-
return &Client{
49+
// Create clientset for typed API access
50+
clientset, err := kubernetes.NewForConfig(config)
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to create clientset: %w", err)
53+
}
54+
55+
client := &Client{
4456
discoveryClient: discoveryClient,
4557
dynamicClient: dynamicClient,
46-
}, nil
58+
clientset: clientset,
59+
}
60+
61+
// Set the default implementation for getPodLogs
62+
client.getPodLogs = client.defaultGetPodLogs
63+
64+
return client, nil
4765
}
4866

4967
// getConfig returns a Kubernetes client configuration
@@ -139,7 +157,13 @@ func (c *Client) SetDiscoveryClient(discoveryClient discovery.DiscoveryInterface
139157
c.discoveryClient = discoveryClient
140158
}
141159

160+
// SetClientset sets the clientset (for testing purposes)
161+
func (c *Client) SetClientset(clientset kubernetes.Interface) {
162+
// Store the interface directly, we'll use it through the interface methods
163+
c.clientset = clientset
164+
}
165+
142166
// IsReady returns true if the client is ready to use
143167
func (c *Client) IsReady() bool {
144-
return c.discoveryClient != nil && c.dynamicClient != nil
168+
return c.discoveryClient != nil && c.dynamicClient != nil && c.clientset != nil
145169
}

pkg/k8s/client_test.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111
"k8s.io/apimachinery/pkg/runtime"
1212
"k8s.io/apimachinery/pkg/runtime/schema"
1313
discoveryfake "k8s.io/client-go/discovery/fake"
14-
"k8s.io/client-go/dynamic/fake"
14+
dynamicfake "k8s.io/client-go/dynamic/fake"
15+
kubefake "k8s.io/client-go/kubernetes/fake"
1516
ktesting "k8s.io/client-go/testing"
1617
)
1718

@@ -24,7 +25,7 @@ func TestListClusteredResources(t *testing.T) {
2425
{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterroles"}: "ClusterRoleList",
2526
}
2627

27-
client := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds)
28+
client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds)
2829

2930
// Create a test client with the fake dynamic client
3031
testClient := &Client{
@@ -84,7 +85,7 @@ func TestListNamespacedResources(t *testing.T) {
8485
{Group: "", Version: "v1", Resource: "services"}: "ServiceList",
8586
}
8687

87-
client := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds)
88+
client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds)
8889

8990
// Create a test client with the fake dynamic client
9091
testClient := &Client{
@@ -140,7 +141,7 @@ func TestListNamespacedResources(t *testing.T) {
140141
func TestApplyClusteredResource(t *testing.T) {
141142
// Create a fake dynamic client
142143
scheme := runtime.NewScheme()
143-
client := fake.NewSimpleDynamicClient(scheme)
144+
client := dynamicfake.NewSimpleDynamicClient(scheme)
144145

145146
// Create a test client with the fake dynamic client
146147
testClient := &Client{
@@ -196,7 +197,7 @@ func TestApplyClusteredResource(t *testing.T) {
196197
func TestApplyNamespacedResource(t *testing.T) {
197198
// Create a fake dynamic client
198199
scheme := runtime.NewScheme()
199-
client := fake.NewSimpleDynamicClient(scheme)
200+
client := dynamicfake.NewSimpleDynamicClient(scheme)
200201

201202
// Create a test client with the fake dynamic client
202203
testClient := &Client{
@@ -254,7 +255,7 @@ func TestApplyNamespacedResource(t *testing.T) {
254255
func TestGetClusteredResource(t *testing.T) {
255256
// Create a fake dynamic client
256257
scheme := runtime.NewScheme()
257-
client := fake.NewSimpleDynamicClient(scheme)
258+
client := dynamicfake.NewSimpleDynamicClient(scheme)
258259

259260
// Create a test client with the fake dynamic client
260261
testClient := &Client{
@@ -307,7 +308,7 @@ func TestGetClusteredResource(t *testing.T) {
307308
func TestGetNamespacedResource(t *testing.T) {
308309
// Create a fake dynamic client
309310
scheme := runtime.NewScheme()
310-
client := fake.NewSimpleDynamicClient(scheme)
311+
client := dynamicfake.NewSimpleDynamicClient(scheme)
311312

312313
// Create a test client with the fake dynamic client
313314
testClient := &Client{
@@ -365,7 +366,7 @@ func TestSetDynamicClient(t *testing.T) {
365366

366367
// Create a fake dynamic client
367368
scheme := runtime.NewScheme()
368-
fakeDynamicClient := fake.NewSimpleDynamicClient(scheme)
369+
fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
369370

370371
// Set the dynamic client
371372
testClient.SetDynamicClient(fakeDynamicClient)
@@ -389,28 +390,35 @@ func TestSetDiscoveryClient(t *testing.T) {
389390
}
390391

391392
func TestIsReady(t *testing.T) {
392-
// Test with both clients nil
393+
// Test with all clients nil
393394
testClient := &Client{}
394-
assert.False(t, testClient.IsReady(), "Expected IsReady to return false when both clients are nil")
395+
assert.False(t, testClient.IsReady(), "Expected IsReady to return false when all clients are nil")
395396

396397
// Test with only dynamic client set
397398
testClient = &Client{}
398399
scheme := runtime.NewScheme()
399-
fakeDynamicClient := fake.NewSimpleDynamicClient(scheme)
400+
fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
400401
testClient.SetDynamicClient(fakeDynamicClient)
401-
assert.False(t, testClient.IsReady(), "Expected IsReady to return false when discovery client is nil")
402+
assert.False(t, testClient.IsReady(), "Expected IsReady to return false when some clients are nil")
402403

403404
// Test with only discovery client set
404405
testClient = &Client{}
405406
fakeDiscoveryClient := &discoveryfake.FakeDiscovery{Fake: &ktesting.Fake{}}
406407
testClient.SetDiscoveryClient(fakeDiscoveryClient)
407-
assert.False(t, testClient.IsReady(), "Expected IsReady to return false when dynamic client is nil")
408+
assert.False(t, testClient.IsReady(), "Expected IsReady to return false when some clients are nil")
408409

409-
// Test with both clients set
410+
// Test with only clientset set
411+
testClient = &Client{}
412+
fakeClientset := kubefake.NewSimpleClientset()
413+
testClient.SetClientset(fakeClientset)
414+
assert.False(t, testClient.IsReady(), "Expected IsReady to return false when some clients are nil")
415+
416+
// Test with all clients set
410417
testClient = &Client{}
411418
testClient.SetDynamicClient(fakeDynamicClient)
412419
testClient.SetDiscoveryClient(fakeDiscoveryClient)
413-
assert.True(t, testClient.IsReady(), "Expected IsReady to return true when both clients are set")
420+
testClient.SetClientset(fakeClientset)
421+
assert.True(t, testClient.IsReady(), "Expected IsReady to return true when all clients are set")
414422
}
415423

416424
func TestListAPIResources(t *testing.T) {

pkg/k8s/subresource.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package k8s
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
7+
"io"
68

9+
corev1 "k8s.io/api/core/v1"
710
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
811
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
912
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -19,6 +22,11 @@ func (c *Client) GetResource(ctx context.Context, gvr schema.GroupVersionResourc
1922
var result *unstructured.Unstructured
2023
var err error
2124

25+
// Special handling for pod logs
26+
if gvr.Resource == "pods" && subresource == "logs" {
27+
return c.getPodLogs(ctx, namespace, name)
28+
}
29+
2230
if namespace == "" {
2331
// Clustered resource
2432
if subresource == "" {
@@ -43,5 +51,48 @@ func (c *Client) GetResource(ctx context.Context, gvr schema.GroupVersionResourc
4351
return nil, fmt.Errorf("failed to get resource: %w", err)
4452
}
4553

54+
return result, nil
55+
}
56+
57+
// defaultGetPodLogs retrieves logs from a pod and returns them as an unstructured object
58+
func (c *Client) defaultGetPodLogs(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
59+
// We need to use the CoreV1 client for logs, as the dynamic client doesn't handle logs properly
60+
podLogOpts := corev1.PodLogOptions{}
61+
62+
// Get the REST client for pods
63+
req := c.clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
64+
65+
// Execute the request
66+
podLogs, err := req.Stream(ctx)
67+
if err != nil {
68+
return nil, fmt.Errorf("failed to get pod logs: %w", err)
69+
}
70+
defer func() {
71+
if closeErr := podLogs.Close(); closeErr != nil {
72+
// Just log the error, we can't return it at this point
73+
fmt.Printf("Error closing pod logs stream: %v\n", closeErr)
74+
}
75+
}()
76+
77+
// Read the logs
78+
buf := new(bytes.Buffer)
79+
_, err = io.Copy(buf, podLogs)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to read pod logs: %w", err)
82+
}
83+
84+
// Create an unstructured object with the logs
85+
result := &unstructured.Unstructured{
86+
Object: map[string]interface{}{
87+
"apiVersion": "v1",
88+
"kind": "Pod",
89+
"metadata": map[string]interface{}{
90+
"name": name,
91+
"namespace": namespace,
92+
},
93+
"logs": buf.String(),
94+
},
95+
}
96+
4697
return result, nil
4798
}

0 commit comments

Comments
 (0)