Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
bfc614f
backend: caching: implementing caching for single-cluster
upsaurav12 Jun 20, 2025
cea00ed
backend: cache: added token in key to make user specific
upsaurav12 Jun 27, 2025
18fa22a
backend: cache: added implementation in different package pkg/cache
upsaurav12 Jun 27, 2025
5e78492
backend: cache: added more unit tests
upsaurav12 Jun 30, 2025
6b3a56b
backend: cache: added more unit tests
upsaurav12 Jun 30, 2025
7c3fc85
backend: cache: move the caching logic to middleware
upsaurav12 Jul 3, 2025
7eb605e
backend: cache: move the caching logic to middleware
upsaurav12 Jul 3, 2025
f9d6377
backend: cache: move the caching logic to middleware
upsaurav12 Jul 3, 2025
ec97252
backend: cache: move the caching logic to middleware
upsaurav12 Jul 3, 2025
8170a17
backend: cache: move the caching logic to middleware
upsaurav12 Jul 8, 2025
6d0b40d
backend: cache: move the caching logic to middleware
upsaurav12 Jul 8, 2025
b5dd9a2
backend: cache: improved IsAllowed implementation
upsaurav12 Jul 8, 2025
a42f7f6
backend: cache: improved IsAllowed implementation
upsaurav12 Jul 8, 2025
b5c7a9e
backend: caching: linting
upsaurav12 Jul 9, 2025
e3b9dc8
backend: caching: fixed null pointer dereference
upsaurav12 Jul 9, 2025
573d088
backend: caching: reused ClientSetWithToken
upsaurav12 Jul 9, 2025
71f8027
backend: caching: replaced once with map to create clientset
upsaurav12 Jul 10, 2025
ca95ecf
backend: caching: replaced once with map to create clientset
upsaurav12 Jul 10, 2025
b936819
backend: headlampConfig: make changes according to conversation
upsaurav12 Jul 14, 2025
fc2463a
backend: caching: improved code
upsaurav12 Jul 15, 2025
171857d
Merge branch 'main' into caching-pagination-search
upsaurav12 Jul 15, 2025
c844f31
backend: caching: added cache flag, removed subrouter logic and X-HEA…
upsaurav12 Jul 16, 2025
b56314f
Merge branch 'caching-pagination-search' of https://github.com/upsaur…
upsaurav12 Jul 16, 2025
576c7f6
backend: caching: fixed linting problem
upsaurav12 Jul 17, 2025
8f26b4f
backend: caching: fixed linting problem
upsaurav12 Jul 17, 2025
5f2ec30
backend: caching: fixed linting problem
upsaurav12 Jul 17, 2025
b6036d5
backend: caching: resolved co-pilot conversation
upsaurav12 Jul 17, 2025
20f5fb5
backend: caching: added more tests
upsaurav12 Jul 21, 2025
cdd718f
backend: caching: added purge login for POST
upsaurav12 Jul 22, 2025
36d0f02
backend: caching: added purge login for POST
upsaurav12 Jul 22, 2025
6f56e57
Update backend/cmd/headlamp.go
upsaurav12 Jul 23, 2025
cdb21c9
backend: caching: added asynchronous solution for purging
upsaurav12 Jul 23, 2025
a93c068
backend: caching: added asynchronous solution for purging
upsaurav12 Jul 23, 2025
eee4fbc
backend: caching: added asynchronous solution for purging
upsaurav12 Jul 23, 2025
3f437bb
backend: caching: added asynchronous solution for purging
upsaurav12 Jul 23, 2025
a536079
backend: caching: added asynchronous solution for purging
upsaurav12 Jul 24, 2025
075803c
backend: caching: added asynchronous solution for purging
upsaurav12 Jul 24, 2025
7985faf
Merge branch 'main' into caching-pagination-search
upsaurav12 Aug 3, 2025
ee03263
Update backend/cmd/headlamp.go
upsaurav12 Jul 23, 2025
546e7a8
backend: k8cache: added logic for cache Invalidation
upsaurav12 Aug 10, 2025
c726361
Update backend/cmd/headlamp.go
upsaurav12 Jul 23, 2025
520fcbc
backend: caching: added asynchronous solution for purging
upsaurav12 Jul 23, 2025
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
61 changes: 31 additions & 30 deletions backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,11 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
"golang.org/x/oauth2"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"

"golang.org/x/oauth2"
)

type HeadlampConfig struct {
Expand Down Expand Up @@ -101,6 +100,8 @@ const (
TokenCacheFileName = "headlamp-token-cache"
)

var k8scache = cache.New[string]()

type clientConfig struct {
Clusters []Cluster `json:"clusters"`
IsDynamicClusterEnabled bool `json:"isDynamicClusterEnabled"`
Expand Down Expand Up @@ -1350,35 +1351,11 @@ func (c *HeadlampConfig) handleError(w http.ResponseWriter, ctx context.Context,
http.Error(w, err.Error(), status)
}

// handleClusterAPI handles cluster API requests. It is responsible for
// all the requests made to /clusters/{clusterName}/{api:.*} endpoint.
// It parses the request and creates a proxy request to the cluster.
// That proxy is saved in the cache with the context key.
func handleClusterAPI(c *HeadlampConfig, router *mux.Router) { //nolint:funlen
router.PathPrefix("/clusters/{clusterName}/{api:.*}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ctx := r.Context()

ctx, span := telemetry.CreateSpan(ctx, r, "cluster-api", "handleClusterAPI",
attribute.String("cluster", mux.Vars(r)["clusterName"]),
)
defer span.End()

c.telemetryHandler.RecordRequestCount(ctx, r, attribute.String("cluster", mux.Vars(r)["clusterName"]))
c.telemetryHandler.RecordEvent(span, "Cluster API request started")

// A deferred function to record duration metrics & log the request completion
defer recordRequestCompletion(c, ctx, start, r)

contextKey, err := c.getContextKeyForRequest(r)
if err != nil {
c.handleError(w, ctx, span, err, "failed to get context key", http.StatusBadRequest)
return
}

kContext, err := c.KubeConfigStore.GetContext(contextKey)
func clusterRequestHandler(c *HeadlampConfig) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span, contextKey, kContext, err := GetContextKeyAndKContext(w, r, c)
if err != nil {
c.handleError(w, ctx, span, err, "failed to get context", http.StatusNotFound)
c.handleError(w, ctx, span, err, "error while retrieving context and kContext", http.StatusBadRequest)
return
}

Expand Down Expand Up @@ -1427,6 +1404,24 @@ func handleClusterAPI(c *HeadlampConfig, router *mux.Router) { //nolint:funlen
})
}

// handleClusterAPI handles cluster API requests. It is responsible for
// all the requests made to /clusters/{clusterName}/{api:.*} endpoint.
// It parses the request and creates a proxy request to the cluster.
// That proxy is saved in the cache with the context key.
func handleClusterAPI(c *HeadlampConfig, router *mux.Router) {
clusterAPIrequest := clusterRequestHandler(c)

handler := clusterAPIrequest

if !c.CacheEnabled {
cacheMiddleware := CacheMiddleWare(c)

handler = cacheMiddleware(handler)
}

router.PathPrefix("/clusters/{clusterName}/{api:.*}").Handler(handler)
}

func recordRequestCompletion(c *HeadlampConfig, ctx context.Context,
start time.Time, r *http.Request,
) {
Expand Down Expand Up @@ -1524,6 +1519,8 @@ func (c *HeadlampConfig) getClusters() []Cluster {
}

for _, context := range contexts {
context := context

if context.Error != "" {
clusters = append(clusters, Cluster{
Name: context.Name,
Expand Down Expand Up @@ -1578,6 +1575,8 @@ func parseCustomNameClusters(contexts []kubeconfig.Context) ([]Cluster, []error)
var setupErrors []error

for _, context := range contexts {
context := context

info := context.KubeContext.Extensions["headlamp_info"]
if info != nil {
// Convert the runtime.Unknown object to a byte slice
Expand Down Expand Up @@ -2038,6 +2037,8 @@ func (c *HeadlampConfig) updateCustomContextToCache(config *api.Config, clusterN
}

for _, context := range contexts {
context := context

// Remove the old context from the store
if err := c.KubeConfigStore.RemoveContext(clusterName); err != nil {
logger.Log(logger.LevelError, nil, err, "Removing context from the store")
Expand Down
105 changes: 105 additions & 0 deletions backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,23 @@ limitations under the License.
package main

import (
"context"
"errors"
"net/http"
"os"
"strings"

"github.com/gorilla/mux"
"github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
"github.com/kubernetes-sigs/headlamp/backend/pkg/config"
"github.com/kubernetes-sigs/headlamp/backend/pkg/headlampconfig"
"github.com/kubernetes-sigs/headlamp/backend/pkg/k8cache"
"github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
"github.com/kubernetes-sigs/headlamp/backend/pkg/plugins"
"github.com/kubernetes-sigs/headlamp/backend/pkg/telemetry"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
Comment on lines +35 to +36
Copy link

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The import order is inconsistent. The golang.org/x/oauth2 import should be grouped with other third-party imports, not placed after the OpenTelemetry imports.

Suggested change
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attribute and trace should be there for creating ctx and span in CacheMiddleware function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@upsaurav12 can you format the file using gofumpt ? I think that should resolve this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes doing it.

)

func main() {
Expand All @@ -50,6 +58,7 @@ func main() {
UseInCluster: conf.InCluster,
KubeConfigPath: conf.KubeConfigPath,
SkippedKubeContexts: conf.SkippedKubeContexts,
CacheEnabled: conf.CacheEnabled,
ListenAddr: conf.ListenAddr,
Port: conf.Port,
DevMode: conf.DevMode,
Expand Down Expand Up @@ -86,6 +95,102 @@ func main() {
})
}

// GetContextKeyAndContext returns Kcontext , ContextKey for using these in CacheMiddleWare function.
// It also return span and ctx that will help while using handleError function.
func GetContextKeyAndKContext(w http.ResponseWriter,
r *http.Request, c *HeadlampConfig) (context.Context,
trace.Span, string, *kubeconfig.Context, error,
) {
ctx := r.Context()
ctx, span := telemetry.CreateSpan(ctx, r, "cluster-api", "handleClusterAPI",
attribute.String("cluster", mux.Vars(r)["clusterName"]),
)

defer span.End()
Copy link

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling 'defer span.End()' inside this helper ends the trace span immediately when this function returns, which may be before downstream logic executes. Consider ending the span in the caller after the handler completes.

Suggested change
defer span.End()
// Note: The caller is responsible for ending the span by calling span.End().

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this span to be instrumented. @illume wdyt? this function just gets the context key and context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have added because i was getting linting error of length of CacheMiddleware function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU telemetry doesn't have anything to do with linting issue. Can you share the exact error that you faced?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, i am talking about funlen linting issue, it was saying that the function length is >60 lines of code, that's why i made a saparate function that will return contextKey , KContext, span and ctx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate function is okay, not every new function needs to have a telemetry span.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay understood, should i need to remove telemetry and span for some new functions such as ReturnAuthErrRespone , ReturnAfterAuthError etc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now, lets remove this.


contextKey, err := c.getContextKeyForRequest(r)
if err != nil {
c.handleError(w, ctx, span, err, "failed to get context Key:", http.StatusBadRequest)
return nil, nil, "", nil, err
}

kContext, err := c.KubeConfigStore.GetContext(contextKey)
if err != nil {
c.handleError(w, ctx, span, err, "failed to get context", http.StatusNotFound)
return nil, nil, "", nil, err
}

return ctx, span, contextKey, kContext, nil
}

// CacheMiddleWare is Middleware for Caching purpose. It involves generating key for a request,
// authorizing user , store resource data in cache and returns data if key is present.
func CacheMiddleWare(c *HeadlampConfig) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span, contextKey, kContext, err := GetContextKeyAndKContext(w, r, c)
if err != nil {
c.handleError(w, ctx, span, err, "failed to get context and Kcontext", http.StatusNotFound)
return
}

if kContext.Error != "" {
c.handleError(w, ctx, span, errors.New(kContext.Error), "context has error", http.StatusBadRequest)
return
}

rcw := k8cache.CreateResponseCapture(w)

key, err := k8cache.GenerateKey(r.URL, contextKey)
if err != nil {
c.handleError(w, ctx, span, err, "failed to generate key ", http.StatusBadRequest)
return
}

isAllowed, authErr := k8cache.IsAllowed(r.URL, kContext, w, r)
if authErr != nil {
k8cache.StoreAfterAuthError(k8scache, isAllowed, next, key, w, r, rcw)

return
} else if !isAllowed {
err := k8cache.ReturnAuthErrorResponse(w, r, contextKey)
if err != nil {
c.handleError(w, ctx, span, err, "error while returning to client", http.StatusInternalServerError)
}

return
}

served, err := k8cache.LoadFromCache(k8scache, isAllowed, key, w, r)
if err != nil {
c.handleError(w, ctx, span, errors.New(kContext.Error), "failed to load from cache", http.StatusServiceUnavailable)
}

if served {
c.telemetryHandler.RecordEvent(span, "Served from cache")
return
}

go k8cache.CheckForChanges(k8scache, contextKey, r, rcw)
next.ServeHTTP(rcw, r)

// // Here after making changes the request goes to CheckAndPurge to check whether
// // the method is any these POST, PUT, PATCH, DELETE. If yes then it
// // is proceed for purge the stale the staled stored requests.
// err = k8cache.CheckAndPurge(w, r, k8scache, next, rcw, isAllowed)
// if err != nil {
// c.handleError(w, ctx, span, err, "error while purging data", http.StatusInternalServerError)
// }

err = k8cache.RequestK8ClusterAPIAndStore(k8scache, r.URL, rcw, r, key)
if err != nil {
c.handleError(w, ctx, span, errors.New(kContext.Error), "error while storing into cache", http.StatusBadRequest)
return
}
})
}
}

func runListPlugins() {
conf, err := config.Parse(os.Args[2:])
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions backend/pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Cache[T any] interface {
Get(ctx context.Context, key string) (T, error)
GetAll(ctx context.Context, selectFunc Matcher) (map[string]T, error)
UpdateTTL(ctx context.Context, key string, ttl time.Duration) error
// Size(ctx context.Context) int
}

// Matcher is a function that returns true if the key matches.
Expand Down Expand Up @@ -169,3 +170,20 @@ func (c *cache[T]) UpdateTTL(ctx context.Context, key string, ttl time.Duration)

return nil
}

// Size returns the number of unexpired items in the cache.
func (c *cache[T]) Size(ctx context.Context) int {
c.lock.RLock()
defer c.lock.RUnlock()

count := 0
now := time.Now()

for _, value := range c.store {
if value.expiresAt.IsZero() || value.expiresAt.After(now) {
count++
}
}

return count
}
2 changes: 2 additions & 0 deletions backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const defaultPort = 4466

type Config struct {
InCluster bool `koanf:"in-cluster"`
CacheEnabled bool `koanf:"cache-enabled"`
DevMode bool `koanf:"dev"`
InsecureSsl bool `koanf:"insecure-ssl"`
EnableHelm bool `koanf:"enable-helm"`
Expand Down Expand Up @@ -237,6 +238,7 @@ func flagset() *flag.FlagSet {
f := flag.NewFlagSet("config", flag.ContinueOnError)

f.Bool("in-cluster", false, "Set when running from a k8s cluster")
f.Bool("cache-enabled", false, "To cache k8s resources")
f.Bool("dev", false, "Allow connections from other origins")
f.Bool("insecure-ssl", false, "Accept/Ignore all server SSL certificates")
f.Bool("enable-dynamic-clusters", false, "Enable dynamic clusters, which stores stateless clusters in the frontend.")
Expand Down
1 change: 1 addition & 0 deletions backend/pkg/headlampconfig/headlampConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type HeadlampCFG struct {
UseInCluster bool
ListenAddr string
DevMode bool
CacheEnabled bool
Insecure bool
EnableHelm bool
EnableDynamicClusters bool
Expand Down
Loading