Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,8 @@ func newHandlers() []handler.Handler {
handler.NewARDHandler(agent.cacheManager),
handler.NewAPDHandler(agent.cacheManager),
handler.NewEnvironmentHandler(agent.cacheManager, agent.cfg.GetCredentialConfig(), envName),
handler.NewDiscoveryManagedApplicationHandler(agent.cacheManager),
handler.NewDiscoveryAccessRequestHandler(agent.cacheManager),
)
case config.TraceabilityAgent:
// Register managed application and access handler for traceability agent
Expand Down
108 changes: 108 additions & 0 deletions pkg/agent/cache/cachevalidation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cache

import (
"time"

v1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1"
management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1"
"github.com/Axway/agent-sdk/pkg/cache"
)

// GetCachedResourcesByKind returns a map of resource name to modification timestamp
// for all cached resources of the given group and kind. When scopeName is non-empty,
// only resources whose scope matches scopeName are included.
func (c *cacheManager) GetCachedResourcesByKind(group, kind, scopeName string) map[string]time.Time {
c.ApplyResourceReadLock()
defer c.ReleaseResourceReadLock()

resourceCache := c.getCacheForKind(kind)
if resourceCache == nil {
// Fall back to the watch resource map for kinds not in a dedicated cache
return c.getWatchResourcesByKind(group, kind, scopeName)
}

return c.extractResourceSummary(resourceCache, scopeName)
}

// getCacheForKind returns the dedicated cache for the given resource kind, or nil
// if the kind is stored in the watch resource map.
func (c *cacheManager) getCacheForKind(kind string) cache.Cache {
switch kind {
case management.APIServiceGVK().Kind:
return c.apiMap
case management.APIServiceInstanceGVK().Kind:
return c.instanceMap
case management.ManagedApplicationGVK().Kind:
return c.managedApplicationMap
case management.AccessRequestGVK().Kind:
return c.accessRequestMap
case management.AccessRequestDefinitionGVK().Kind:
return c.ardMap
case management.CredentialRequestDefinitionGVK().Kind:
return c.crdMap
case management.ApplicationProfileDefinitionGVK().Kind:
return c.apdMap
case management.ComplianceRuntimeResultGVK().Kind:
return c.crrMap
default:
return nil
}
}

// ResourceCacheKey builds a unique cache key from kind, scope name, and resource name.
func ResourceCacheKey(kind, scopeName, name string) string {
return kind + "/" + scopeName + "/" + name
}

// extractResourceSummary iterates the cache keys and returns a composite key -> modifyTimestamp.
// When scopeName is non-empty, only resources whose scope matches scopeName are included.
func (c *cacheManager) extractResourceSummary(resourceCache cache.Cache, scopeName string) map[string]time.Time {
result := make(map[string]time.Time)
keys := resourceCache.GetKeys()

for _, key := range keys {
item, err := resourceCache.Get(key)
if err != nil {
continue
}
ri, ok := item.(*v1.ResourceInstance)
if !ok || ri == nil {
continue
}
if scopeName != "" && ri.Metadata.Scope.Name != scopeName {
continue
}
modTime := time.Time(ri.Metadata.Audit.ModifyTimestamp)
result[ResourceCacheKey(ri.Kind, ri.Metadata.Scope.Name, ri.Name)] = modTime
}

return result
}

// getWatchResourcesByKind iterates the watch resource map and returns resources
// matching the given group and kind. When scopeName is non-empty, only resources
// whose scope matches scopeName are included.
func (c *cacheManager) getWatchResourcesByKind(group, kind, scopeName string) map[string]time.Time {
result := make(map[string]time.Time)

keys := c.watchResourceMap.GetKeys()
for _, key := range keys {
item, err := c.watchResourceMap.Get(key)
if err != nil {
continue
}
ri, ok := item.(*v1.ResourceInstance)
if !ok || ri == nil {
continue
}
if scopeName != "" && ri.Metadata.Scope.Name != scopeName {
continue
}
if ri.Group == group && ri.Kind == kind {
modTime := time.Time(ri.Metadata.Audit.ModifyTimestamp)
result[ResourceCacheKey(ri.Kind, ri.Metadata.Scope.Name, ri.Name)] = modTime
}
}

return result
}
221 changes: 221 additions & 0 deletions pkg/agent/cache/cachevalidation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package cache

import (
"testing"
"time"

v1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1"
management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1"
defs "github.com/Axway/agent-sdk/pkg/apic/definitions"
"github.com/Axway/agent-sdk/pkg/config"
"github.com/stretchr/testify/assert"
)

func makeRI(group, kind, scopeKind, scopeName, name, id string, modTime time.Time) *v1.ResourceInstance {
return &v1.ResourceInstance{
ResourceMeta: v1.ResourceMeta{
GroupVersionKind: v1.GroupVersionKind{
GroupKind: v1.GroupKind{
Group: group,
Kind: kind,
},
APIVersion: "v1alpha1",
},
Metadata: v1.Metadata{
ID: id,
Scope: v1.MetadataScope{
Kind: scopeKind,
Name: scopeName,
},
Audit: v1.AuditMetadata{
ModifyTimestamp: v1.Time(modTime),
},
},
Name: name,
},
}
}

func makeAPIServiceRI(scopeName, name, apiID string, modTime time.Time) *v1.ResourceInstance {
ri := makeRI("management", management.APIServiceGVK().Kind, "Environment", scopeName, name, apiID, modTime)
ri.SubResources = map[string]interface{}{
defs.XAgentDetails: map[string]interface{}{
defs.AttrExternalAPIID: apiID,
defs.AttrExternalAPIName: name,
},
}
return ri
}

func TestResourceCacheKey(t *testing.T) {
tests := map[string]struct {
kind string
scope string
name string
expected string
}{
"with scope": {kind: "APIService", scope: "env1", name: "svc1", expected: "APIService/env1/svc1"},
"empty scope": {kind: "APIService", scope: "", name: "svc1", expected: "APIService//svc1"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.expected, ResourceCacheKey(tc.kind, tc.scope, tc.name))
})
}
}

func TestGetCachedResourcesByKind(t *testing.T) {
modTime := time.Date(2026, 3, 12, 10, 0, 0, 0, time.UTC)
modTime2 := time.Date(2026, 3, 12, 11, 0, 0, 0, time.UTC)

type testCase struct {
setup func(Manager)
group string
kind string
scopeName string
expectLen int
expectKeys []string
verify func(t *testing.T, result map[string]time.Time)
}

tests := map[string]testCase{
"APIService": {
setup: func(cm Manager) {
cm.AddAPIService(makeAPIServiceRI("env1", "svc1", "ext-id-1", modTime))
},
group: "management",
kind: management.APIServiceGVK().Kind,
expectLen: 1,
expectKeys: []string{ResourceCacheKey(management.APIServiceGVK().Kind, "env1", "svc1")},
verify: func(t *testing.T, result map[string]time.Time) {
assert.Equal(t, modTime, result[ResourceCacheKey(management.APIServiceGVK().Kind, "env1", "svc1")])
},
},
"APIServiceInstance": {
setup: func(cm Manager) {
cm.AddAPIServiceInstance(makeRI("management", management.APIServiceInstanceGVK().Kind, "Environment", "env1", "inst1", "id1", modTime))
},
group: "management",
kind: management.APIServiceInstanceGVK().Kind,
expectLen: 1,
expectKeys: []string{ResourceCacheKey(management.APIServiceInstanceGVK().Kind, "env1", "inst1")},
},
"ManagedApplication": {
setup: func(cm Manager) {
cm.AddManagedApplication(makeRI("management", management.ManagedApplicationGVK().Kind, "Environment", "env1", "app1", "id1", modTime))
},
group: "management",
kind: management.ManagedApplicationGVK().Kind,
expectLen: 1,
expectKeys: []string{ResourceCacheKey(management.ManagedApplicationGVK().Kind, "env1", "app1")},
},
"AccessRequest": {
setup: func(cm Manager) {
cm.AddAccessRequest(makeRI("management", management.AccessRequestGVK().Kind, "Environment", "env1", "ar1", "id1", modTime))
},
group: "management",
kind: management.AccessRequestGVK().Kind,
expectLen: 1,
expectKeys: []string{ResourceCacheKey(management.AccessRequestGVK().Kind, "env1", "ar1")},
},
"multiple resources same kind": {
setup: func(cm Manager) {
cm.AddAPIService(makeAPIServiceRI("env1", "svc1", "ext-id-1", modTime))
cm.AddAPIService(makeAPIServiceRI("env1", "svc2", "ext-id-2", modTime2))
},
group: "management",
kind: management.APIServiceGVK().Kind,
expectLen: 2,
expectKeys: []string{
ResourceCacheKey(management.APIServiceGVK().Kind, "env1", "svc1"),
ResourceCacheKey(management.APIServiceGVK().Kind, "env1", "svc2"),
},
},
"different scopes - no scope filter returns all": {
setup: func(cm Manager) {
cm.AddAPIService(makeAPIServiceRI("env1", "svc1", "ext-id-1", modTime))
cm.AddAPIService(makeAPIServiceRI("env2", "svc1", "ext-id-2", modTime))
},
group: "management",
kind: management.APIServiceGVK().Kind,
expectLen: 2,
expectKeys: []string{
ResourceCacheKey(management.APIServiceGVK().Kind, "env1", "svc1"),
ResourceCacheKey(management.APIServiceGVK().Kind, "env2", "svc1"),
},
},
"different scopes - scoped to env1 returns only env1": {
setup: func(cm Manager) {
cm.AddAPIService(makeAPIServiceRI("env1", "svc1", "ext-id-1", modTime))
cm.AddAPIService(makeAPIServiceRI("env2", "svc1", "ext-id-2", modTime))
},
group: "management",
kind: management.APIServiceGVK().Kind,
scopeName: "env1",
expectLen: 1,
expectKeys: []string{ResourceCacheKey(management.APIServiceGVK().Kind, "env1", "svc1")},
},
"watch resource scope filter": {
setup: func(cm Manager) {
cm.AddWatchResource(makeRI("catalog", "SomeKind", "Environment", "env1", "res1", "id1", modTime))
cm.AddWatchResource(makeRI("catalog", "SomeKind", "Environment", "env2", "res2", "id2", modTime))
},
group: "catalog",
kind: "SomeKind",
scopeName: "env1",
expectLen: 1,
expectKeys: []string{ResourceCacheKey("SomeKind", "env1", "res1")},
},
"empty cache": {
group: "management",
kind: management.APIServiceGVK().Kind,
expectLen: 0,
},
"unknown kind falls back to watch resource": {
setup: func(cm Manager) {
cm.AddWatchResource(makeRI("catalog", "SomeCustomKind", "Environment", "env1", "custom1", "id1", modTime))
},
group: "catalog",
kind: "SomeCustomKind",
expectLen: 1,
expectKeys: []string{ResourceCacheKey("SomeCustomKind", "env1", "custom1")},
},
"watch resource filters by group and kind - group A": {
setup: func(cm Manager) {
cm.AddWatchResource(makeRI("groupA", "KindA", "", "", "res1", "id1", modTime))
cm.AddWatchResource(makeRI("groupB", "KindB", "", "", "res2", "id2", modTime))
},
group: "groupA",
kind: "KindA",
expectLen: 1,
expectKeys: []string{ResourceCacheKey("KindA", "", "res1")},
},
"watch resource filters by group and kind - group B": {
setup: func(cm Manager) {
cm.AddWatchResource(makeRI("groupA", "KindA", "", "", "res1", "id1", modTime))
cm.AddWatchResource(makeRI("groupB", "KindB", "", "", "res2", "id2", modTime))
},
group: "groupB",
kind: "KindB",
expectLen: 1,
expectKeys: []string{ResourceCacheKey("KindB", "", "res2")},
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
cm := NewAgentCacheManager(&config.CentralConfiguration{}, false)
if tc.setup != nil {
tc.setup(cm)
}
result := cm.GetCachedResourcesByKind(tc.group, tc.kind, tc.scopeName)
assert.Len(t, result, tc.expectLen)
for _, k := range tc.expectKeys {
assert.Contains(t, result, k)
}
if tc.verify != nil {
tc.verify(t, result)
}
})
}
}
3 changes: 3 additions & 0 deletions pkg/agent/cache/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"os"
"sync"
"time"

defs "github.com/Axway/agent-sdk/pkg/apic/definitions"

Expand Down Expand Up @@ -130,6 +131,8 @@ type Manager interface {
GetWatchResourceByName(group, kind, name string) *v1.ResourceInstance
DeleteWatchResource(group, kind, id string) error

GetCachedResourcesByKind(group, kind, scopeName string) map[string]time.Time

ApplyResourceReadLock()
ReleaseResourceReadLock()
}
Expand Down
Loading
Loading