Files
actions-runner-controller/controllers/actions.github.com/resourcecache_test.go
T

498 lines
18 KiB
Go

package actionsgithubcom
import (
"fmt"
"testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func newTestResourceCache() *ResourceCache {
cache := NewResourceCache()
return &cache
}
func resourceCacheHasMainObjectEntries(cache *ResourceCache, mainObject client.Object) bool {
return resourceCacheStateHasMainObjectEntries(cache.autoscalingListener, mainObject) ||
resourceCacheStateHasMainObjectEntries(cache.ephemeralRunnerSet, mainObject) ||
resourceCacheStateHasMainObjectEntries(cache.listenerPod, mainObject) ||
resourceCacheStateHasMainObjectEntries(cache.listenerServiceAccount, mainObject) ||
resourceCacheStateHasMainObjectEntries(cache.listenerRole, mainObject) ||
resourceCacheStateHasMainObjectEntries(cache.listenerRoleBinding, mainObject)
}
func resourceCacheStateHasMainObjectEntries[T client.Object](state *resourceCacheState[T], mainObject client.Object) bool {
uid := mainObject.GetUID()
if uid == "" {
return false
}
state.mu.RLock()
defer state.mu.RUnlock()
return len(state.entriesByMainUID[uid]) > 0
}
func TestResourceCacheUpsertReplacesByDependencyResourceVersion(t *testing.T) {
mainObject := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
ResourceVersion: "10",
},
}
desiredPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
ResourceVersion: "1",
Labels: map[string]string{
"app": "listener",
},
},
}
configSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "listener-config",
Namespace: "controller-ns",
UID: "config-secret-uid",
ResourceVersion: "1",
},
}
serviceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "service-account-uid",
ResourceVersion: "1",
},
}
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "scale-set-ns",
UID: "role-uid",
ResourceVersion: "1",
},
}
cache := NewResourceCache()
value, replaced := cache.listenerPod.Upsert(mainObject, desiredPod, configSecret, serviceAccount, role)
assert.True(t, replaced)
_, ok := cache.listenerPod.Get(mainObject, desiredPod, configSecret, serviceAccount, role)
assert.True(t, ok)
assert.Equal(t, "1", value.ResourceVersion)
_, replaced = cache.listenerPod.Upsert(mainObject, desiredPod, role, configSecret, serviceAccount)
assert.False(t, replaced, "dependency ordering should not affect the cache value")
_, ok = cache.listenerPod.Get(mainObject, desiredPod, configSecret, serviceAccount, role)
assert.True(t, ok)
configSecret.ResourceVersion = "2"
value, replaced = cache.listenerPod.Upsert(mainObject, desiredPod, configSecret, serviceAccount, role)
assert.True(t, replaced)
staleConfigSecret := configSecret.DeepCopy()
staleConfigSecret.ResourceVersion = "1"
_, ok = cache.listenerPod.Get(mainObject, desiredPod, staleConfigSecret, serviceAccount, role)
assert.False(t, ok)
_, ok = cache.listenerPod.Get(mainObject, desiredPod, configSecret, serviceAccount, role)
assert.True(t, ok)
assert.Same(t, desiredPod, value.Object)
}
func TestResourceCacheDeleteRemovesMainObjectEntries(t *testing.T) {
mainObject := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
},
}
otherMainObject := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "other-listener",
Namespace: "controller-ns",
UID: "other-listener-uid",
},
}
listenerPod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "listener", Namespace: "controller-ns"}}
listenerServiceAccount := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "listener", Namespace: "controller-ns"}}
otherListenerPod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "other-listener", Namespace: "controller-ns"}}
cache := NewResourceCache()
cache.listenerPod.Upsert(mainObject, listenerPod)
cache.listenerServiceAccount.Upsert(mainObject, listenerServiceAccount)
cache.listenerPod.Upsert(otherMainObject, otherListenerPod)
cache.Delete(mainObject)
_, ok := cache.listenerPod.Get(mainObject, listenerPod)
assert.False(t, ok)
_, ok = cache.listenerServiceAccount.Get(mainObject, listenerServiceAccount)
assert.False(t, ok)
_, ok = cache.listenerPod.Get(otherMainObject, otherListenerPod)
assert.True(t, ok)
}
func TestResourceCacheDeletePanicsWithNilCache(t *testing.T) {
var cache *ResourceCache
assert.Panics(t, func() {
cache.Delete(&v1alpha1.AutoscalingListener{})
})
}
func TestResourceCacheIgnoresInvalidInputs(t *testing.T) {
cache := NewResourceCache()
desiredPod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "listener", Namespace: "controller-ns"}}
mainObjectWithoutUID := &v1alpha1.AutoscalingListener{ObjectMeta: metav1.ObjectMeta{Name: "listener", Namespace: "controller-ns"}}
mainObject := mainObjectWithoutUID.DeepCopy()
mainObject.UID = "listener-uid"
_, replaced := cache.listenerPod.Upsert(mainObjectWithoutUID, desiredPod)
assert.False(t, replaced)
_, ok := cache.listenerPod.Get(mainObjectWithoutUID, desiredPod)
assert.False(t, ok)
var nilDependency *corev1.Secret
assert.NotPanics(t, func() {
_, replaced = cache.listenerPod.Upsert(mainObject, desiredPod, nilDependency)
assert.False(t, replaced)
_, ok = cache.listenerPod.Get(mainObject, desiredPod, nilDependency)
assert.False(t, ok)
})
tooManyDependencies := make([]client.Object, resourceCacheMaxDependencyRefs+1)
for i := range tooManyDependencies {
tooManyDependencies[i] = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("dependency-%d", i), Namespace: "controller-ns"}}
}
assert.NotPanics(t, func() {
_, replaced = cache.listenerPod.Upsert(mainObject, desiredPod, tooManyDependencies...)
assert.False(t, replaced)
_, ok = cache.listenerPod.Get(mainObject, desiredPod, tooManyDependencies...)
assert.False(t, ok)
})
}
func TestResourceBuilderCachesListenerPodDependencies(t *testing.T) {
listener := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
Annotations: map[string]string{"example.com/listener-hash": "listener-hash"},
},
Spec: v1alpha1.AutoscalingListenerSpec{
Image: "listener:latest",
AutoscalingRunnerSetName: "scale-set",
AutoscalingRunnerSetNamespace: "scale-set-ns",
EphemeralRunnerSetName: "scale-set",
},
}
podConfig := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "listener-config",
Namespace: "controller-ns",
UID: "config-secret-uid",
ResourceVersion: "11",
Annotations: map[string]string{"example.com/config-hash": "config-hash"},
},
}
serviceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "service-account-uid",
ResourceVersion: "12",
Annotations: map[string]string{"example.com/service-account-hash": "service-account-hash"},
},
}
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "scale-set-ns",
UID: "role-uid",
ResourceVersion: "13",
Annotations: map[string]string{"example.com/role-hash": "role-hash"},
},
}
roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "scale-set-ns",
UID: "role-binding-uid",
ResourceVersion: "14",
Annotations: map[string]string{"example.com/role-binding-hash": "role-binding-hash"},
},
}
cache := NewResourceCache()
b := ResourceBuilder{ResourceCache: &cache}
listenerPod, err := b.newScaleSetListenerPod(listener, podConfig, serviceAccount, role, roleBinding, nil)
require.NoError(t, err)
metadataDependency := resourceCacheObjectMetadataInputObject(listener)
cachedPod, ok := b.ResourceCache.listenerPod.Get(listener, listenerPod, podConfig, serviceAccount, role, roleBinding, metadataDependency)
require.True(t, ok)
assert.IsType(t, &corev1.Pod{}, cachedPod)
lookupPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: listenerPod.Name,
Namespace: listenerPod.Namespace,
},
}
cachedPod, ok = b.ResourceCache.listenerPod.Get(listener, lookupPod, podConfig, serviceAccount, role, roleBinding, metadataDependency)
require.True(t, ok, "name-only lookup object should hit the cached desired pod")
assert.Same(t, listenerPod, cachedPod)
role.ResourceVersion = "changed"
_, ok = b.ResourceCache.listenerPod.Get(listener, lookupPod, podConfig, serviceAccount, role, roleBinding, metadataDependency)
assert.False(t, ok)
}
func TestResourceBuilderCachesListenerPodMetadataDependency(t *testing.T) {
listener := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
Labels: map[string]string{
"arc.test/listener-label": "initial",
},
},
Spec: v1alpha1.AutoscalingListenerSpec{
Image: "listener:latest",
AutoscalingRunnerSetName: "scale-set",
AutoscalingRunnerSetNamespace: "scale-set-ns",
EphemeralRunnerSetName: "scale-set",
},
}
podConfig := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "listener-config", Namespace: "controller-ns", UID: "config-secret-uid", ResourceVersion: "11"}}
serviceAccount := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "listener", Namespace: "controller-ns", UID: "service-account-uid", ResourceVersion: "12"}}
role := &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{Name: "listener", Namespace: "scale-set-ns", UID: "role-uid", ResourceVersion: "13"}}
roleBinding := &rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{Name: "listener", Namespace: "scale-set-ns", UID: "role-binding-uid", ResourceVersion: "14"}}
cache := NewResourceCache()
b := ResourceBuilder{ResourceCache: &cache}
listenerPod, err := b.newScaleSetListenerPod(listener, podConfig, serviceAccount, role, roleBinding, nil)
require.NoError(t, err)
lookupPod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: listenerPod.Name, Namespace: listenerPod.Namespace}}
_, ok := b.ResourceCache.listenerPod.Get(listener, lookupPod, podConfig, serviceAccount, role, roleBinding, resourceCacheObjectMetadataInputObject(listener))
assert.True(t, ok)
listener.Labels["arc.test/listener-label"] = "updated"
_, ok = b.ResourceCache.listenerPod.Get(listener, lookupPod, podConfig, serviceAccount, role, roleBinding, resourceCacheObjectMetadataInputObject(listener))
assert.False(t, ok, "cache miss when listener metadata used by the pod changes")
}
func TestResourceBuilderCachesEphemeralRunnerSet(t *testing.T) {
autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "scale-set",
Namespace: "default",
UID: "scale-set-uid",
Annotations: map[string]string{
runnerScaleSetIDAnnotationKey: "1",
},
},
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/actions/actions-runner-controller",
},
}
cache := NewResourceCache()
b := ResourceBuilder{ResourceCache: &cache}
runnerSet, err := b.newEphemeralRunnerSet(&autoscalingRunnerSet)
require.NoError(t, err)
metadataDependency := resourceCacheObjectMetadataInputObject(&autoscalingRunnerSet)
cachedRunnerSet, ok := b.ResourceCache.ephemeralRunnerSet.Get(&autoscalingRunnerSet, runnerSet, metadataDependency)
require.True(t, ok, "direct cache Get with returned object should hit")
assert.Equal(t, runnerSet.Spec, cachedRunnerSet.Spec)
assert.Same(t, runnerSet, cachedRunnerSet)
lookupRunnerSet := &v1alpha1.EphemeralRunnerSet{ObjectMeta: metav1.ObjectMeta{Name: runnerSet.Name, Namespace: runnerSet.Namespace}}
cachedRunnerSet, ok = b.ResourceCache.ephemeralRunnerSet.Get(&autoscalingRunnerSet, lookupRunnerSet, metadataDependency)
require.True(t, ok, "name-only lookup object should hit the cached desired runner set")
assert.Same(t, runnerSet, cachedRunnerSet)
autoscalingRunnerSet.Annotations[runnerScaleSetIDAnnotationKey] = "2"
_, ok = b.ResourceCache.ephemeralRunnerSet.Get(&autoscalingRunnerSet, lookupRunnerSet, metadataDependency)
assert.True(t, ok, "cache should be valid when main object generation unchanged")
}
func TestResourceBuilderCachesEphemeralRunnerSetMetadataDependency(t *testing.T) {
autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "scale-set",
Namespace: "default",
UID: "scale-set-uid",
Labels: map[string]string{
"arc.test/scale-set-label": "initial",
},
Annotations: map[string]string{
runnerScaleSetIDAnnotationKey: "1",
},
},
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/actions/actions-runner-controller",
},
}
cache := NewResourceCache()
b := ResourceBuilder{ResourceCache: &cache}
runnerSet, err := b.newEphemeralRunnerSet(&autoscalingRunnerSet)
require.NoError(t, err)
lookupRunnerSet := &v1alpha1.EphemeralRunnerSet{ObjectMeta: metav1.ObjectMeta{Name: runnerSet.Name, Namespace: runnerSet.Namespace}}
_, ok := b.ResourceCache.ephemeralRunnerSet.Get(&autoscalingRunnerSet, lookupRunnerSet, resourceCacheObjectMetadataInputObject(&autoscalingRunnerSet))
assert.True(t, ok)
autoscalingRunnerSet.Labels["arc.test/scale-set-label"] = "updated"
_, ok = b.ResourceCache.ephemeralRunnerSet.Get(&autoscalingRunnerSet, lookupRunnerSet, resourceCacheObjectMetadataInputObject(&autoscalingRunnerSet))
assert.False(t, ok, "cache miss when autoscaling runner set metadata used by the runner set changes")
}
func TestResourceCacheOwnerGenerationDoesNotAffectCacheEntry(t *testing.T) {
mainObject := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
Generation: 5,
},
}
desiredPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
},
}
cache := NewResourceCache()
_, replaced := cache.listenerPod.Upsert(mainObject, desiredPod)
assert.True(t, replaced)
_, ok := cache.listenerPod.Get(mainObject, desiredPod)
assert.True(t, ok)
mainObjectCopy := mainObject.DeepCopy()
_, ok = cache.listenerPod.Get(mainObjectCopy, desiredPod)
assert.True(t, ok, "cache hit when owner generation unchanged")
}
func TestResourceCacheOwnerGenerationChangeInvalidatesCacheEntry(t *testing.T) {
mainObject := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
Generation: 5,
},
}
desiredPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
},
}
cache := NewResourceCache()
_, replaced := cache.listenerPod.Upsert(mainObject, desiredPod)
assert.True(t, replaced)
_, ok := cache.listenerPod.Get(mainObject, desiredPod)
assert.True(t, ok)
mainObjectWithNewGeneration := mainObject.DeepCopy()
mainObjectWithNewGeneration.Generation = 6
_, ok = cache.listenerPod.Get(mainObjectWithNewGeneration, desiredPod)
assert.False(t, ok, "cache miss when owner generation changes")
}
func TestResourceCacheDependencyResourceVersionChangeInvalidates(t *testing.T) {
mainObject := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
},
}
desiredPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
},
}
dependency := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "config",
Namespace: "controller-ns",
UID: "config-uid",
ResourceVersion: "1",
},
}
cache := NewResourceCache()
_, replaced := cache.listenerPod.Upsert(mainObject, desiredPod, dependency)
assert.True(t, replaced)
_, ok := cache.listenerPod.Get(mainObject, desiredPod, dependency)
assert.True(t, ok)
dependencyWithNewResourceVersion := dependency.DeepCopy()
dependencyWithNewResourceVersion.ResourceVersion = "2"
_, ok = cache.listenerPod.Get(mainObject, desiredPod, dependencyWithNewResourceVersion)
assert.False(t, ok, "cache miss when dependency resourceVersion changes")
}
func TestResourceCacheNoAnnotationFallback(t *testing.T) {
mainObject := &v1alpha1.AutoscalingListener{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
},
}
desiredPodWithoutResourceVersion := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
},
}
desiredPodWithDifferentAnnotation := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
Annotations: map[string]string{
"unrelated-key": "unrelated-value",
},
},
}
cache := NewResourceCache()
_, replaced := cache.listenerPod.Upsert(mainObject, desiredPodWithoutResourceVersion)
assert.True(t, replaced)
_, ok := cache.listenerPod.Get(mainObject, desiredPodWithoutResourceVersion)
assert.True(t, ok, "cache hit with same pod object")
_, ok = cache.listenerPod.Get(mainObject, desiredPodWithDifferentAnnotation)
assert.False(t, ok, "cache miss when pod changed - uses hash not annotation fallback")
desiredPodIdentical := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "listener",
Namespace: "controller-ns",
},
}
_, ok = cache.listenerPod.Get(mainObject, desiredPodIdentical)
assert.True(t, ok, "cache hit when pod structure identical even if different instance")
}