Skip to content

Commit bf69e92

Browse files
fix: enable kubelet HTTP API with proper 501 responses for unsupported operations
The kubelet HTTPS listener on :10250 was never started because the VK library's runHTTP() requires both TLSConfig and Handler to be set on NodeConfig — neither was, so it silently exited on every startup. This meant kubectl logs, kubectl exec, kubectl port-forward, and kubectl attach all resulted in connection refused rather than a meaningful HTTP error. Any metrics scraper hitting the kubelet endpoint also got nothing. Changes: - cmd/cisco-vk/run.go: generate a self-signed ECDSA TLS certificate at startup and set it on NodeConfig.TLSConfig so runHTTP() starts the listener. Build a custom PodHandlerConfig with nil for all unsupported operations (logs, exec, attach, port-forward, stats, metrics) so the VK library's built-in NotImplemented handler returns HTTP 501 instead of calling through to the provider stub and returning HTTP 500. Uses a closure pattern to wire the mux after the provider is created while satisfying the Handler requirement before NewNode() runs. - internal/provider/provider.go: fix AttachToContainer to return a proper error instead of writing a hardcoded string and returning nil — the prior implementation kept streaming connections open indefinitely. HTTP status codes after this change: GET /pods → 200 (was: connection refused) GET /containerLogs/... → 501 Not Implemented (was: connection refused) POST /exec/... → 501 Not Implemented (was: connection refused) POST /attach/... → 501 Not Implemented (was: connection refused) POST /portForward/... → 501 Not Implemented (was: connection refused) GET /stats/summary → 404 route not registered (was: connection refused) GET /metrics/resource → 404 route not registered (was: connection refused) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dc0b0ed commit bf69e92

2 files changed

Lines changed: 119 additions & 9 deletions

File tree

cmd/cisco-vk/run.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,23 @@ package main
1616

1717
import (
1818
"context"
19+
"crypto/ecdsa"
20+
"crypto/elliptic"
21+
"crypto/rand"
22+
"crypto/tls"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"encoding/pem"
1926
"fmt"
27+
"math/big"
28+
"net"
29+
"net/http"
2030
"os"
2131
"os/signal"
2232
"path"
2333
"runtime"
2434
"syscall"
35+
"time"
2536

2637
"github.com/cisco/virtual-kubelet-cisco/internal/config"
2738
"github.com/cisco/virtual-kubelet-cisco/internal/drivers"
@@ -31,10 +42,13 @@ import (
3142
"github.com/virtual-kubelet/virtual-kubelet/log"
3243
"github.com/virtual-kubelet/virtual-kubelet/log/logrus"
3344
"github.com/virtual-kubelet/virtual-kubelet/node"
45+
"github.com/virtual-kubelet/virtual-kubelet/node/api"
3446
"github.com/virtual-kubelet/virtual-kubelet/node/nodeutil"
47+
v1 "k8s.io/api/core/v1"
3548
"k8s.io/client-go/kubernetes"
3649
"k8s.io/client-go/rest"
3750
"k8s.io/client-go/tools/clientcmd"
51+
"k8s.io/apimachinery/pkg/labels"
3852
)
3953

4054
// Interface Guards
@@ -192,12 +206,33 @@ func runVirtualKubelet(cmd *cobra.Command, args []string) error {
192206
}
193207
effectiveNodeName = provider.GetNodeName(effectiveNodeName, appCfg.Device.Address)
194208

209+
tlsCfg, err := generateSelfSignedTLS(appCfg.Device.Address)
210+
if err != nil {
211+
return fmt.Errorf("failed to generate TLS config for kubelet API: %w", err)
212+
}
213+
214+
// innerHandler is set inside newProviderFunc (after the provider is created) and
215+
// read by the handlerWrapper below. This closure pattern lets us satisfy the
216+
// NodeConfig.Handler requirement before the provider exists, while still wiring
217+
// the real mux once the provider is available.
218+
var innerHandler http.Handler
219+
220+
handlerWrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
221+
if innerHandler != nil {
222+
innerHandler.ServeHTTP(w, r)
223+
return
224+
}
225+
http.Error(w, "provider not yet initialised", http.StatusServiceUnavailable)
226+
})
227+
195228
opts := []nodeutil.NodeOpt{
196229
nodeutil.WithNodeConfig(nodeutil.NodeConfig{
197230
Client: clientset,
198231
NodeSpec: provider.GetInitialNodeSpec(effectiveNodeName, appCfg.Device.Address),
199232
HTTPListenAddr: ":10250",
200233
NumWorkers: 5,
234+
TLSConfig: tlsCfg,
235+
Handler: handlerWrapper,
201236
}),
202237
}
203238

@@ -214,6 +249,29 @@ func runVirtualKubelet(cmd *cobra.Command, args []string) error {
214249
if err != nil {
215250
return nil, nil, fmt.Errorf("failed to initialise PodHandler: %w", err)
216251
}
252+
253+
// Build a custom PodHandlerConfig that only wires supported operations.
254+
// Unsupported methods are left nil so the VK library returns HTTP 501
255+
// automatically via its built-in NotImplemented handler, rather than
256+
// calling through to the provider stub and returning HTTP 500.
257+
mux := http.NewServeMux()
258+
mux.Handle("/", api.PodHandler(api.PodHandlerConfig{
259+
GetPods: podHandler.GetPods,
260+
GetPodsFromKubernetes: func(ctx context.Context) ([]*v1.Pod, error) {
261+
return vkCfg.Pods.List(labels.Everything())
262+
},
263+
StreamIdleTimeout: 0,
264+
StreamCreationTimeout: 0,
265+
// Explicitly nil — library returns HTTP 501 for each of these:
266+
RunInContainer: nil,
267+
AttachToContainer: nil,
268+
GetContainerLogs: nil,
269+
PortForward: nil,
270+
GetStatsSummary: nil,
271+
GetMetricsResource: nil,
272+
}, true))
273+
innerHandler = mux
274+
217275
return podHandler, nodeHandler, nil
218276
}
219277
n, err := nodeutil.NewNode(effectiveNodeName, newProviderFunc, opts...)
@@ -228,3 +286,63 @@ func runVirtualKubelet(cmd *cobra.Command, args []string) error {
228286
log.G(ctx).Info("Cisco Virtual Kubelet stopped")
229287
return nil
230288
}
289+
290+
// generateSelfSignedTLS creates a self-signed ECDSA certificate for the kubelet
291+
// HTTPS listener on :10250. The certificate includes SANs for the device address,
292+
// 127.0.0.1, and localhost so that both local and remote health checks work.
293+
func generateSelfSignedTLS(deviceAddr string) (*tls.Config, error) {
294+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
295+
if err != nil {
296+
return nil, fmt.Errorf("generate ECDSA key: %w", err)
297+
}
298+
299+
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
300+
if err != nil {
301+
return nil, fmt.Errorf("generate serial number: %w", err)
302+
}
303+
304+
tmpl := &x509.Certificate{
305+
SerialNumber: serial,
306+
Subject: pkix.Name{
307+
Organization: []string{"Cisco Virtual Kubelet"},
308+
CommonName: "cisco-virtual-kubelet",
309+
},
310+
NotBefore: time.Now().Add(-time.Minute),
311+
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), // 10 years
312+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
313+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
314+
BasicConstraintsValid: true,
315+
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
316+
DNSNames: []string{"localhost"},
317+
}
318+
319+
// Include the device address as a SAN if it's an IP.
320+
if ip := net.ParseIP(deviceAddr); ip != nil {
321+
tmpl.IPAddresses = append(tmpl.IPAddresses, ip)
322+
} else if deviceAddr != "" {
323+
tmpl.DNSNames = append(tmpl.DNSNames, deviceAddr)
324+
}
325+
326+
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
327+
if err != nil {
328+
return nil, fmt.Errorf("create certificate: %w", err)
329+
}
330+
331+
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
332+
333+
keyDER, err := x509.MarshalECPrivateKey(key)
334+
if err != nil {
335+
return nil, fmt.Errorf("marshal EC private key: %w", err)
336+
}
337+
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
338+
339+
cert, err := tls.X509KeyPair(certPEM, keyPEM)
340+
if err != nil {
341+
return nil, fmt.Errorf("load key pair: %w", err)
342+
}
343+
344+
return &tls.Config{
345+
Certificates: []tls.Certificate{cert},
346+
MinVersion: tls.VersionTLS12,
347+
}, nil
348+
}

internal/provider/provider.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,7 @@ func (p *AppHostingProvider) GetPods(ctx context.Context) ([]*v1.Pod, error) {
150150
}
151151

152152
func (p *AppHostingProvider) AttachToContainer(ctx context.Context, namespace, podName, containerName string, attach api.AttachIO) error {
153-
// log.G(ctx).Infof("Attaching to container %s in pod %s/%s", containerName, namespace, podName)
154-
155-
// For Cisco IOx containers, attachment is limited
156-
// We can simulate it by providing a shell prompt
157-
if attach.Stdout() != nil {
158-
attach.Stdout().Write([]byte("Cisco IOx container attachment not fully supported\n"))
159-
}
160-
161-
return nil
153+
return fmt.Errorf("AttachToContainer is not supported by the Cisco Virtual Kubelet")
162154
}
163155

164156
// NOT YET IMPLEMENTED

0 commit comments

Comments
 (0)