Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4a97c60
feat: add backend caching for AMT API endpoints
nmgaston Jan 23, 2026
3287fdc
Merge branch 'main' into backendCaching
nmgaston Jan 23, 2026
177381f
fix: remove unused time imports from devices package
nmgaston Jan 23, 2026
722eb74
fix: resolve golangci-lint issues (godot, nlreturn, wsl_v5)
nmgaston Jan 23, 2026
b2c3fc7
fix: add periods to remaining godoc comments in keys.go
nmgaston Jan 23, 2026
6868be0
feat: add combined KVM initialization endpoint
nmgaston Jan 23, 2026
e96a3e4
feat: add Prometheus and Grafana to docker-compose
nmgaston Jan 23, 2026
2f48991
chore: format files
nmgaston Jan 27, 2026
a079f42
fix: add missing methods to ws/v1 Feature interface and regenerate mocks
nmgaston Jan 27, 2026
7bc9d8d
fix: create prometheus.yml in CI workflow before docker-compose
nmgaston Jan 27, 2026
f233389
chore: format files
nmgaston Jan 27, 2026
bdd17d5
fix: replace magic number with constant for KVM init cache TTL
nmgaston Jan 27, 2026
1b62eff
fix: only start required services in CI, exclude prometheus/grafana
nmgaston Jan 27, 2026
dfddae5
refactor: remove prometheus and grafana from docker-compose
nmgaston Jan 27, 2026
0e166fb
revert: remove unnecessary quotes from AUTH_DISABLED value
nmgaston Jan 27, 2026
c7a13bc
Merge branch 'main' into backendCaching
nmgaston Jan 30, 2026
4330058
chore: remove caching debug statements
nmgaston Feb 3, 2026
51de2c5
Merge branch 'backendCaching' of https://github.com/nmgaston/console …
nmgaston Feb 3, 2026
aac928d
chore: fix lint issue
nmgaston Feb 3, 2026
21bd4c6
feat: implement backend caching with robfig/go-cache for improved per…
nmgaston Feb 4, 2026
1856a09
Merge branch 'main' into backendCaching
nmgaston Feb 5, 2026
1088fb2
feat: implement backend caching with robfig/go-cache for improved per…
nmgaston Feb 4, 2026
25ae30b
Merge branch 'main' into backendCaching
nmgaston Feb 20, 2026
4a1a1fe
Merge branch 'main' into backendCaching
nmgaston Feb 26, 2026
8030abe
Merge branch 'main' into backendCaching
nmgaston Mar 16, 2026
512d0b8
Merge branch 'main' into backendCaching
nmgaston Mar 19, 2026
9e7832b
Merge branch 'main' into backendCaching
nmgaston Mar 23, 2026
d2c57d4
Merge branch 'main' into backendCaching
nmgaston Mar 23, 2026
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
117 changes: 117 additions & 0 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package cache

import (
"sync"
"time"
)

const (
// CleanupInterval is how often expired cache entries are removed.
CleanupInterval = 30 * time.Second
// PowerStateTTL is the cache duration for power state (changes frequently).
PowerStateTTL = 5 * time.Second
// FeaturesTTL is the cache duration for features (rarely changes).
FeaturesTTL = 30 * time.Second
// KVMTTL is the cache duration for KVM display settings (rarely changes).
KVMTTL = 30 * time.Second
)

// Entry represents a cached value with expiration.
type Entry struct {
Value interface{}
ExpiresAt time.Time
}

// Cache is a simple in-memory cache with TTL support.
type Cache struct {
mu sync.RWMutex
items map[string]Entry
}

// New creates a new Cache instance.
func New() *Cache {
c := &Cache{
items: make(map[string]Entry),
}
// Start cleanup goroutine
go c.cleanupExpired()

return c
}

// Set stores a value in the cache with the given TTL.
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()

c.items[key] = Entry{
Value: value,
ExpiresAt: time.Now().Add(ttl),
}
}

// Get retrieves a value from the cache.
// Returns the value and true if found and not expired, nil and false otherwise.
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()

entry, found := c.items[key]
if !found {
return nil, false
}

if time.Now().After(entry.ExpiresAt) {
return nil, false
}

return entry.Value, true
}

// Delete removes a value from the cache.
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()

delete(c.items, key)
}

// DeletePattern removes all keys matching a pattern (simple prefix match).
func (c *Cache) DeletePattern(prefix string) {
c.mu.Lock()
defer c.mu.Unlock()

for key := range c.items {
if len(key) >= len(prefix) && key[:len(prefix)] == prefix {
delete(c.items, key)
}
}
}

// Clear removes all items from the cache.
func (c *Cache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()

c.items = make(map[string]Entry)
}

// cleanupExpired runs periodically to remove expired entries.
func (c *Cache) cleanupExpired() {
ticker := time.NewTicker(CleanupInterval)
defer ticker.Stop()

for range ticker.C {
c.mu.Lock()

now := time.Now()

for key, entry := range c.items {
if now.After(entry.ExpiresAt) {
delete(c.items, key)
}
}

c.mu.Unlock()
}
}
46 changes: 46 additions & 0 deletions internal/cache/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cache

import "fmt"

// Cache key prefixes.
const (
PrefixFeatures = "features:"
PrefixPowerState = "power:"
PrefixKVMDisplay = "kvm:display:"
PrefixKVMInit = "kvm:init:"
PrefixGeneral = "general:"
)

// MakeFeaturesKey creates a cache key for device features.
func MakeFeaturesKey(guid string) string {
return fmt.Sprintf("%s%s", PrefixFeatures, guid)
}

// MakePowerStateKey creates a cache key for power state.
func MakePowerStateKey(guid string) string {
return fmt.Sprintf("%s%s", PrefixPowerState, guid)
}

// MakeKVMDisplayKey creates a cache key for KVM displays.
func MakeKVMDisplayKey(guid string) string {
return fmt.Sprintf("%s%s", PrefixKVMDisplay, guid)
}

// MakeGeneralSettingsKey creates a cache key for general settings.
func MakeGeneralSettingsKey(guid string) string {
return fmt.Sprintf("%s%s", PrefixGeneral, guid)
}

// MakeKVMInitKey creates a cache key for KVM initialization data.
func MakeKVMInitKey(guid string) string {
return fmt.Sprintf("%s%s", PrefixKVMInit, guid)
}

// InvalidateDeviceCache removes all cached data for a device.
func InvalidateDeviceCache(c *Cache, guid string) {
c.Delete(MakeFeaturesKey(guid))
c.Delete(MakePowerStateKey(guid))
c.Delete(MakeKVMDisplayKey(guid))
c.Delete(MakeKVMInitKey(guid))
c.Delete(MakeGeneralSettingsKey(guid))
}
3 changes: 3 additions & 0 deletions internal/controller/httpapi/v1/devicemanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func NewAmtRoutes(handler *gin.RouterGroup, d devices.Feature, amt amtexplorer.F
h.GET("kvm/displays/:guid", r.getKVMDisplays)
h.PUT("kvm/displays/:guid", r.setKVMDisplays)

// KVM initialization - combines display, power, redirection, and features
h.GET("kvm/init/:guid", r.getKVMInitData)

// Network link preference
h.POST("network/linkPreference/:guid", r.setLinkPreference)
}
Expand Down
24 changes: 24 additions & 0 deletions internal/controller/httpapi/v1/kvminit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package v1

import (
"net/http"

"github.com/gin-gonic/gin"
)

// getKVMInitData returns all data needed to initialize a KVM session.
// This combines display settings, power state, redirection status, and features
// into a single API call to reduce latency.
func (r *deviceManagementRoutes) getKVMInitData(c *gin.Context) {
guid := c.Param("guid")

initData, err := r.d.GetKVMInitData(c.Request.Context(), guid)
if err != nil {
r.l.Error(err, "http - v1 - getKVMInitData")
ErrorResponse(c, err)

return
}

c.JSON(http.StatusOK, initData)
}
49 changes: 49 additions & 0 deletions internal/controller/openapi/devicemanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ func (f *FuegoAdapter) registerKVMAndCertificateRoutes() {
fuego.OptionPath("guid", "Device GUID"),
)

// kvm initialization
fuego.Get(f.server, "/api/v1/admin/kvm/init/{guid}", f.getKVMInitData,
fuego.OptionTags("Device Management"),
fuego.OptionSummary("Get KVM initialization data"),
fuego.OptionDescription("Retrieve all data needed to initialize a KVM session (display settings, power state, redirection status, and features) in a single call"),
fuego.OptionPath("guid", "Device GUID"),
)

// Certificates
fuego.Get(f.server, "/api/v1/admin/amt/certificates/{guid}", f.getCertificates,
fuego.OptionTags("Device Management"),
Expand Down Expand Up @@ -299,6 +307,47 @@ func (f *FuegoAdapter) setKVMDisplays(c fuego.ContextWithBody[dto.KVMScreenSetti
return dto.KVMScreenSettings{Displays: []dto.KVMScreenDisplay{display}}, nil
}

func (f *FuegoAdapter) getKVMInitData(_ fuego.ContextNoBody) (dto.KVMInitResponse, error) {
return dto.KVMInitResponse{
DisplaySettings: dto.KVMScreenSettings{
Displays: []dto.KVMScreenDisplay{
{
DisplayIndex: 0,
IsActive: true,
ResolutionX: 1920,
ResolutionY: 1080,
UpperLeftX: 0,
UpperLeftY: 0,
Role: "primary",
IsDefault: true,
},
},
},
PowerState: dto.PowerState{
PowerState: 2,
OSPowerSavingState: 0,
},
RedirectionStatus: dto.KVMRedirectionStatus{
IsSOLConnected: false,
IsIDERConnected: false,
},
Features: dto.GetFeaturesResponse{
Redirection: true,
KVM: true,
SOL: true,
IDER: true,
OptInState: 0,
UserConsent: "none",
KVMAvailable: true,
OCR: false,
HTTPSBootSupported: false,
WinREBootSupported: false,
LocalPBABootSupported: false,
RemoteErase: false,
},
}, nil
}

func (f *FuegoAdapter) getCertificates(_ fuego.ContextNoBody) (dto.SecuritySettings, error) {
return dto.SecuritySettings{}, nil
}
Expand Down
4 changes: 4 additions & 0 deletions internal/controller/ws/v1/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,8 @@ type Feature interface {
// KVM Screen Settings
GetKVMScreenSettings(c context.Context, guid string) (dto.KVMScreenSettings, error)
SetKVMScreenSettings(c context.Context, guid string, req dto.KVMScreenSettingsRequest) (dto.KVMScreenSettings, error)
// KVM Initialization Data - combined endpoint for display, power, redirection, and features
GetKVMInitData(c context.Context, guid string) (dto.KVMInitResponse, error)
// Link Preference (AMT_EthernetPortSettings)
SetLinkPreference(c context.Context, guid string, req dto.LinkPreferenceRequest) (dto.LinkPreferenceResponse, error)
}
16 changes: 16 additions & 0 deletions internal/entity/dto/v1/kvminit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dto

// KVMInitResponse combines all data needed to initialize KVM session.
// This reduces multiple API calls into a single request.
type KVMInitResponse struct {
DisplaySettings KVMScreenSettings `json:"displaySettings"`
PowerState PowerState `json:"powerState"`
RedirectionStatus KVMRedirectionStatus `json:"redirectionStatus"`
Features GetFeaturesResponse `json:"features"`
}

// KVMRedirectionStatus represents the status of redirection services.
type KVMRedirectionStatus struct {
IsSOLConnected bool `json:"isSOLConnected"`
IsIDERConnected bool `json:"isIDERConnected"`
}
25 changes: 8 additions & 17 deletions internal/mocks/amtexplorer_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading