Remove the integrity hash annotation (#4643)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Nikola Jokic
2026-09-11 18:27:55 +02:00
committed by GitHub
co-authored by Copilot App
parent 484564e6d6
commit 03328aa14f
7 changed files with 147 additions and 139 deletions
@@ -46,15 +46,6 @@ var commonLabelKeys = [...]string{
LabelKeyGitHubRepository,
}
// annotationKeyIntegrityHash is used as a hash of the important fields
// of each resource to determine if more drastic action should be taken.
//
// For example, annotations/labels are not something that should modify
// the behavior of a resource, while the change in spec is. Therefore,
// the spec hash should contain the spec fields in order to determine
// modifications.
const annotationKeyIntegrityHash = "actions.github.com/integrity-hash"
const labelValueKubernetesPartOf = "gha-runner-scale-set"
var (
@@ -185,9 +176,7 @@ func (b *ResourceBuilder) newAutoscalingListener(autoscalingRunnerSet *v1alpha1.
return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
}
annotations := map[string]string{
annotationKeyIntegrityHash: spec.Hash(),
}
var annotations map[string]string
if autoscalingRunnerSet.Spec.AutoscalingListenerMetadata != nil {
labels = b.filterAndMergeLabels(autoscalingRunnerSet.Spec.AutoscalingListenerMetadata.Labels, labels)
@@ -322,8 +311,6 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
},
}
desiredSecret.Annotations[annotationKeyIntegrityHash] = scaleSetListenerConfigIntegrityHash(desiredSecret)
if err := b.setControllerReference(autoscalingListener, desiredSecret); err != nil {
return nil, fmt.Errorf("failed to set controller reference for listener config secret: %w", err)
}
@@ -331,18 +318,6 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
return desiredSecret, nil
}
func scaleSetListenerConfigIntegrityHash(secret *corev1.Secret) string {
type data struct {
Data map[string][]byte `json:"data,omitempty"`
}
d := data{
Data: secret.Data,
}
return hash.ComputeTemplateHash(&d)
}
func (b *ResourceBuilder) newScaleSetListenerPod(
autoscalingListener *v1alpha1.AutoscalingListener,
podConfig *corev1.Secret,
@@ -643,8 +618,6 @@ func (b *ResourceBuilder) newScaleSetListenerServiceAccount(autoscalingListener
base.Annotations = b.mergeAnnotations(autoscalingListener.Spec.ServiceAccountMetadata.Annotations, base.Annotations)
}
base.Annotations[annotationKeyIntegrityHash] = scaleSetListenerServiceAccountIntegrityHash(base)
if err := b.setControllerReference(autoscalingListener, base); err != nil {
return nil, fmt.Errorf("failed to set controller reference for listener service account: %w", err)
}
@@ -653,22 +626,6 @@ func (b *ResourceBuilder) newScaleSetListenerServiceAccount(autoscalingListener
return base, nil
}
func scaleSetListenerServiceAccountIntegrityHash(sa *corev1.ServiceAccount) string {
type data struct {
Secrets []corev1.ObjectReference `json:"secrets"`
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets"`
AutomountServiceAccountToken *bool `json:"automountServiceAccountToken"`
}
d := data{
Secrets: sa.Secrets,
ImagePullSecrets: sa.ImagePullSecrets,
AutomountServiceAccountToken: sa.AutomountServiceAccountToken,
}
return hash.ComputeTemplateHash(&d)
}
func (b *ResourceBuilder) newScaleSetListenerRole(autoscalingListener *v1alpha1.AutoscalingListener) *rbacv1.Role {
cacheKeyObject := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
@@ -707,24 +664,11 @@ func (b *ResourceBuilder) newScaleSetListenerRole(autoscalingListener *v1alpha1.
Rules: rulesForListenerRole([]string{autoscalingListener.Spec.EphemeralRunnerSetName}),
}
newRole.Annotations[annotationKeyIntegrityHash] = scaleSetRoleIntegrityHash(newRole)
b.ResourceCache.listenerRole.Upsert(autoscalingListener, newRole)
return newRole
}
func scaleSetRoleIntegrityHash(role *rbacv1.Role) string {
type data struct {
Rules []rbacv1.PolicyRule `json:"rules"`
}
d := data{
Rules: role.Rules,
}
return hash.ComputeTemplateHash(&d)
}
func (b *ResourceBuilder) newScaleSetListenerRoleBinding(autoscalingListener *v1alpha1.AutoscalingListener, listenerRole *rbacv1.Role, serviceAccount *corev1.ServiceAccount) *rbacv1.RoleBinding {
cacheKeyObject := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
@@ -777,26 +721,11 @@ func (b *ResourceBuilder) newScaleSetListenerRoleBinding(autoscalingListener *v1
Subjects: subjects,
}
newRoleBinding.Annotations[annotationKeyIntegrityHash] = scaleSetListenerRoleBindingIntegrityHash(newRoleBinding)
b.ResourceCache.listenerRoleBinding.Upsert(autoscalingListener, newRoleBinding, listenerRole, serviceAccount)
return newRoleBinding
}
func scaleSetListenerRoleBindingIntegrityHash(rb *rbacv1.RoleBinding) string {
type data struct {
RoleRef rbacv1.RoleRef `json:"roleRef"`
Subjects []rbacv1.Subject `json:"subjects"`
}
d := data{
RoleRef: rb.RoleRef,
Subjects: rb.Subjects,
}
return hash.ComputeTemplateHash(&d)
}
func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*v1alpha1.EphemeralRunnerSet, error) {
runnerScaleSetID, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIDAnnotationKey])
if err != nil {
@@ -886,8 +815,6 @@ func (b *ResourceBuilder) newAutoscalingListenerProxySecret(autoscalingListener
Data: data,
}
newProxySecret.Annotations[annotationKeyIntegrityHash] = autoscalingListenerProxySecretIntegrityHash(newProxySecret)
if err := b.setControllerReference(autoscalingListener, newProxySecret); err != nil {
return nil, fmt.Errorf("failed to set controller reference for listener proxy secret: %w", err)
}
@@ -895,18 +822,6 @@ func (b *ResourceBuilder) newAutoscalingListenerProxySecret(autoscalingListener
return newProxySecret, nil
}
func autoscalingListenerProxySecretIntegrityHash(secret *corev1.Secret) string {
type data struct {
Data map[string][]byte `json:"data"`
}
d := data{
Data: secret.Data,
}
return hash.ComputeTemplateHash(&d)
}
func (b *ResourceBuilder) newEphemeralRunner(ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet) (*v1alpha1.EphemeralRunner, error) {
labels := make(map[string]string, len(ephemeralRunnerSet.Labels))
maps.Copy(labels, ephemeralRunnerSet.Labels)
@@ -1053,8 +968,6 @@ func (b *ResourceBuilder) newEphemeralRunnerSetProxySecret(ephemeralRunnerSet *v
Data: data,
}
runnerPodProxySecret.Annotations[annotationKeyIntegrityHash] = ephemeralRunnerSetProxySecretZIdentityHash(runnerPodProxySecret)
if err := b.setControllerReference(ephemeralRunnerSet, runnerPodProxySecret); err != nil {
return nil, fmt.Errorf("failed to set controller reference for ephemeral runner set proxy secret: %w", err)
}
@@ -1062,18 +975,6 @@ func (b *ResourceBuilder) newEphemeralRunnerSetProxySecret(ephemeralRunnerSet *v
return runnerPodProxySecret, nil
}
func ephemeralRunnerSetProxySecretZIdentityHash(secret *corev1.Secret) string {
type data struct {
Data map[string][]byte `json:"data"`
}
d := data{
Data: secret.Data,
}
return hash.ComputeTemplateHash(&d)
}
func scaleSetListenerConfigName(autoscalingListener *v1alpha1.AutoscalingListener) string {
return autoscalingListener.Name + "-config"
}
@@ -0,0 +1,138 @@
package actionsgithubcom
import (
"testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// legacyIntegrityHashAnnotation is the annotation this package used to stamp on
// the resources it builds. It is deliberately spelled out rather than
// referenced, because the constant it mirrors has been deleted; these tests
// pin the upgrade behaviour for objects that were created while it still
// existed.
const legacyIntegrityHashAnnotation = "actions.github.com/integrity-hash"
func newLegacyAnnotationTestAutoscalingRunnerSet() *v1alpha1.AutoscalingRunnerSet {
return &v1alpha1.AutoscalingRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scale-set",
Namespace: "test-ns",
Labels: map[string]string{
LabelKeyKubernetesPartOf: labelValueKubernetesPartOf,
LabelKeyKubernetesVersion: "0.2.0",
},
Annotations: map[string]string{
runnerScaleSetIDAnnotationKey: "1",
AnnotationKeyGitHubRunnerGroupName: "test-group",
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
},
},
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/org/repo",
},
}
}
// TestLegacyIntegrityHashAnnotationCausesOneTimeListenerRecreation documents an
// upgrade consequence of no longer stamping the integrity hash.
//
// AutoscalingRunnerSetReconciler compares the live listener's annotations
// against the desired ones with cmp.Equal and deletes the listener when they
// differ. A listener created by an older controller carries the legacy
// annotation, the desired listener no longer does, so the first reconcile after
// an upgrade recreates it.
//
// This is a one-time rollout, not a reconcile loop: the replacement is built by
// the same code path and carries no annotation, so the next comparison matches.
// In practice the rollout is not additional either, since the listener runs the
// manager's own image, and a controller upgrade changes that image and so
// already forces the same recreation through the Spec comparison.
func TestLegacyIntegrityHashAnnotationCausesOneTimeListenerRecreation(t *testing.T) {
autoscalingRunnerSet := newLegacyAnnotationTestAutoscalingRunnerSet()
cache := NewResourceCache()
b := ResourceBuilder{ResourceCache: &cache}
ephemeralRunnerSet, err := b.newEphemeralRunnerSet(autoscalingRunnerSet)
require.NoError(t, err)
desired, err := b.newAutoscalingListener(autoscalingRunnerSet, ephemeralRunnerSet, "controller-ns", "test:latest", nil)
require.NoError(t, err)
assert.NotContains(t, desired.Annotations, legacyIntegrityHashAnnotation)
// A listener as an older controller would have left it behind.
live := desired.DeepCopy()
live.Annotations = map[string]string{legacyIntegrityHashAnnotation: "some-stale-hash"}
assert.False(
t,
cmp.Equal(live.Annotations, desired.Annotations),
"a listener carrying the legacy annotation must not compare equal to the desired listener, otherwise it would never be replaced",
)
// The replacement converges, so the recreation happens once rather than on
// every reconcile.
replacement, err := b.newAutoscalingListener(autoscalingRunnerSet, ephemeralRunnerSet, "controller-ns", "test:latest", nil)
require.NoError(t, err)
assert.NotContains(t, replacement.Annotations, legacyIntegrityHashAnnotation)
assert.True(
t,
cmp.Equal(replacement.Annotations, desired.Annotations),
"the recreated listener must match the desired one, otherwise the controller would rebuild it forever",
)
}
// TestLegacyIntegrityHashAnnotationRetainedOnExistingObjects pins the scope of
// this removal: it stops the annotation being written, it does not strip it
// from objects that already carry it.
//
// Every reconciler that updates an object it does not replace merges the live
// annotations under the desired ones, so a key that is only absent from the
// desired set survives. Nothing reads the annotation any more, so it is inert
// metadata rather than a behavioural leftover; removing it from existing
// objects would mean patching every managed object on upgrade and is a separate
// decision from this change.
func TestLegacyIntegrityHashAnnotationRetainedOnExistingObjects(t *testing.T) {
ephemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scale-set",
Namespace: "test-ns",
Labels: map[string]string{
LabelKeyGitHubScaleSetName: "test-scale-set",
LabelKeyGitHubScaleSetNamespace: "test-ns",
},
},
}
var b ResourceBuilder
desired, err := b.newEphemeralRunnerSetProxySecret(ephemeralRunnerSet, map[string][]byte{
"http_proxy": []byte("http://proxy.example.com"),
})
require.NoError(t, err)
require.NotContains(t, desired.Annotations, legacyIntegrityHashAnnotation)
// A proxy secret as an older controller would have left it behind.
live := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: desired.Name,
Namespace: desired.Namespace,
Annotations: map[string]string{
legacyIntegrityHashAnnotation: "some-stale-hash",
"example.com/user-annotation": "user-value",
},
},
}
merged := b.mergeAnnotations(live.Annotations, desired.Annotations)
assert.Contains(t, merged, legacyIntegrityHashAnnotation,
"the legacy annotation survives on existing objects; this change stops it being written, it does not migrate it away")
assert.Equal(t, "user-value", merged["example.com/user-annotation"],
"unrelated annotations must be preserved")
}
@@ -115,7 +115,7 @@ func TestMetadataPropagation(t *testing.T) {
assert.Equal(t, labelValueKubernetesPartOf, ephemeralRunnerSet.Labels[LabelKeyKubernetesPartOf])
assert.Equal(t, "runner-set", ephemeralRunnerSet.Labels[LabelKeyKubernetesComponent])
assert.Equal(t, autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], ephemeralRunnerSet.Labels[LabelKeyKubernetesVersion])
assert.NotContains(t, ephemeralRunnerSet.Annotations, annotationKeyIntegrityHash)
assert.NotContains(t, ephemeralRunnerSet.Annotations, "actions.github.com/integrity-hash")
assert.Equal(t, autoscalingRunnerSet.Name, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetName])
assert.Equal(t, autoscalingRunnerSet.Namespace, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetNamespace])
assert.Equal(t, "", ephemeralRunnerSet.Labels[LabelKeyGitHubEnterprise])
@@ -132,7 +132,7 @@ func TestMetadataPropagation(t *testing.T) {
assert.Equal(t, labelValueKubernetesPartOf, listener.Labels[LabelKeyKubernetesPartOf])
assert.Equal(t, "runner-scale-set-listener", listener.Labels[LabelKeyKubernetesComponent])
assert.Equal(t, autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], listener.Labels[LabelKeyKubernetesVersion])
assert.NotEmpty(t, listener.Annotations[annotationKeyIntegrityHash])
assert.NotContains(t, listener.Annotations, "actions.github.com/integrity-hash")
assert.Equal(t, autoscalingRunnerSet.Name, listener.Labels[LabelKeyGitHubScaleSetName])
assert.Equal(t, autoscalingRunnerSet.Namespace, listener.Labels[LabelKeyGitHubScaleSetNamespace])
assert.Equal(t, "", listener.Labels[LabelKeyGitHubEnterprise])
@@ -206,7 +206,7 @@ func TestMetadataPropagation(t *testing.T) {
}
}
func TestEphemeralRunnerSetProxySecretZIdentityHash(t *testing.T) {
func TestEphemeralRunnerSetProxySecretMetadata(t *testing.T) {
ephemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scale-set",
@@ -224,13 +224,11 @@ func TestEphemeralRunnerSetProxySecretZIdentityHash(t *testing.T) {
})
require.NoError(t, err)
actualHash := proxySecret.Annotations[annotationKeyIntegrityHash]
assert.NotEmpty(t, actualHash)
assert.Equal(t, ephemeralRunnerSetProxySecretZIdentityHash(proxySecret), actualHash)
changedProxySecret := proxySecret.DeepCopy()
changedProxySecret.Data["http_proxy"] = []byte("http://updated-proxy.example.com")
assert.NotEqual(t, actualHash, ephemeralRunnerSetProxySecretZIdentityHash(changedProxySecret))
assert.Equal(t, proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet), proxySecret.Name)
assert.Equal(t, ephemeralRunnerSet.Namespace, proxySecret.Namespace)
assert.Equal(t, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetName], proxySecret.Labels[LabelKeyGitHubScaleSetName])
assert.Equal(t, ephemeralRunnerSet.Labels[LabelKeyGitHubScaleSetNamespace], proxySecret.Labels[LabelKeyGitHubScaleSetNamespace])
assert.NotContains(t, proxySecret.Annotations, "actions.github.com/integrity-hash")
}
func TestGitHubURLTrimLabelValues(t *testing.T) {
@@ -318,7 +316,6 @@ func TestOwnershipRelationships(t *testing.T) {
runnerScaleSetIDAnnotationKey: "1",
AnnotationKeyGitHubRunnerGroupName: "test-group",
AnnotationKeyGitHubRunnerScaleSetName: "test-scale-set",
annotationKeyIntegrityHash: "test-hash",
},
},
Spec: v1alpha1.AutoscalingRunnerSetSpec{
@@ -246,9 +246,6 @@ func (k resourceCacheDependencyKey) Equal(other resourceCacheDependencyKey) bool
func newResourceCacheObjectRef(object client.Object) ResourceCacheObjectRef {
resourceVersion := object.GetResourceVersion()
if resourceVersion == "" {
resourceVersion = object.GetAnnotations()[annotationKeyIntegrityHash]
}
if resourceVersion == "" {
resourceVersion = hash.ComputeTemplateHash(object)
}
@@ -187,9 +187,6 @@ func TestResourceBuilderCachesListenerPodDependencies(t *testing.T) {
Name: "listener",
Namespace: "controller-ns",
UID: "listener-uid",
Annotations: map[string]string{
annotationKeyIntegrityHash: "listener-hash",
},
},
Spec: v1alpha1.AutoscalingListenerSpec{
Image: "listener:latest",
@@ -204,9 +201,6 @@ func TestResourceBuilderCachesListenerPodDependencies(t *testing.T) {
Namespace: "controller-ns",
UID: "config-secret-uid",
ResourceVersion: "11",
Annotations: map[string]string{
annotationKeyIntegrityHash: "config-hash",
},
},
}
serviceAccount := &corev1.ServiceAccount{
@@ -215,9 +209,6 @@ func TestResourceBuilderCachesListenerPodDependencies(t *testing.T) {
Namespace: "controller-ns",
UID: "service-account-uid",
ResourceVersion: "12",
Annotations: map[string]string{
annotationKeyIntegrityHash: "service-account-hash",
},
},
}
role := &rbacv1.Role{
@@ -226,9 +217,6 @@ func TestResourceBuilderCachesListenerPodDependencies(t *testing.T) {
Namespace: "scale-set-ns",
UID: "role-uid",
ResourceVersion: "13",
Annotations: map[string]string{
annotationKeyIntegrityHash: "role-hash",
},
},
}
roleBinding := &rbacv1.RoleBinding{
@@ -237,9 +225,6 @@ func TestResourceBuilderCachesListenerPodDependencies(t *testing.T) {
Namespace: "scale-set-ns",
UID: "role-binding-uid",
ResourceVersion: "14",
Annotations: map[string]string{
annotationKeyIntegrityHash: "role-binding-hash",
},
},
}