Compare commits

...

6 Commits

Author SHA1 Message Date
Nikola Jokic
4bea1ebf10 remove mirror secret 2025-04-17 13:30:33 +02:00
Nikola Jokic
c36c141185 pull in updated listener 2025-04-14 11:00:15 +02:00
Nikola Jokic
8a8d279aba Re-create the listener when GitHub secret is updated 2025-04-14 10:35:52 +02:00
Nikola Jokic
15990d492d Include more context to errors raised by github/actions client (#4032)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-11 11:36:15 +02:00
dependabot[bot]
462db4dfc8 Bump the gomod group across 1 directory with 7 updates (#4008)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
2025-04-07 16:51:07 +02:00
David Maxwell
ea27448da5 Fix busy runners metric (#4016) 2025-04-04 17:17:09 +02:00
14 changed files with 340 additions and 230 deletions

View File

@@ -279,10 +279,10 @@ type AutoscalingRunnerSetStatus struct {
FailedEphemeralRunners int `json:"failedEphemeralRunners"`
}
func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
func (ars *AutoscalingRunnerSet) ListenerSpecHash(githubSecret *corev1.Secret) string {
arsSpec := ars.Spec.DeepCopy()
spec := arsSpec
return hash.ComputeTemplateHash(&spec)
secret := githubSecret.DeepCopy()
return hash.ComputeCombinedObjectsHash(&arsSpec, &secret)
}
func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string {

View File

@@ -331,7 +331,7 @@ func (e *exporter) PublishStatistics(stats *actions.RunnerScaleSetStatistic) {
e.setGauge(MetricAssignedJobs, e.scaleSetLabels, float64(stats.TotalAssignedJobs))
e.setGauge(MetricRunningJobs, e.scaleSetLabels, float64(stats.TotalRunningJobs))
e.setGauge(MetricRegisteredRunners, e.scaleSetLabels, float64(stats.TotalRegisteredRunners))
e.setGauge(MetricBusyRunners, e.scaleSetLabels, float64(float64(stats.TotalRegisteredRunners)))
e.setGauge(MetricBusyRunners, e.scaleSetLabels, float64(float64(stats.TotalBusyRunners)))
e.setGauge(MetricIdleRunners, e.scaleSetLabels, float64(stats.TotalIdleRunners))
}

View File

@@ -137,27 +137,6 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
return ctrl.Result{}, err
}
// Create a mirror secret in the same namespace as the AutoscalingListener
mirrorSecret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerSecretMirrorName(autoscalingListener)}, mirrorSecret); err != nil {
if !kerrors.IsNotFound(err) {
log.Error(err, "Unable to get listener secret mirror", "namespace", autoscalingListener.Namespace, "name", scaleSetListenerSecretMirrorName(autoscalingListener))
return ctrl.Result{}, err
}
// Create a mirror secret for the listener pod in the Controller namespace for listener pod to use
log.Info("Creating a mirror listener secret for the listener pod")
return r.createSecretsForListener(ctx, autoscalingListener, secret, log)
}
// make sure the mirror secret is up to date
mirrorSecretDataHash := mirrorSecret.Labels["secret-data-hash"]
secretDataHash := hash.ComputeTemplateHash(secret.Data)
if mirrorSecretDataHash != secretDataHash {
log.Info("Updating mirror listener secret for the listener pod", "mirrorSecretDataHash", mirrorSecretDataHash, "secretDataHash", secretDataHash)
return r.updateSecretsForListener(ctx, secret, mirrorSecret, log)
}
// Make sure the runner scale set listener service account is created for the listener pod in the controller namespace
serviceAccount := new(corev1.ServiceAccount)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerServiceAccountName(autoscalingListener)}, serviceAccount); err != nil {
@@ -239,7 +218,7 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
// Create a listener pod in the controller namespace
log.Info("Creating a listener pod")
return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, mirrorSecret, log)
return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, secret, log)
}
cs := listenerContainerStatus(listenerPod)
@@ -601,22 +580,6 @@ func (r *AutoscalingListenerReconciler) createProxySecret(ctx context.Context, a
return ctrl.Result{Requeue: true}, nil
}
func (r *AutoscalingListenerReconciler) updateSecretsForListener(ctx context.Context, secret *corev1.Secret, mirrorSecret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
dataHash := hash.ComputeTemplateHash(secret.Data)
updatedMirrorSecret := mirrorSecret.DeepCopy()
updatedMirrorSecret.Labels["secret-data-hash"] = dataHash
updatedMirrorSecret.Data = secret.Data
logger.Info("Updating listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name, "hash", dataHash)
if err := r.Update(ctx, updatedMirrorSecret); err != nil {
logger.Error(err, "Unable to update listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name)
return ctrl.Result{}, err
}
logger.Info("Updated listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name, "hash", dataHash)
return ctrl.Result{Requeue: true}, nil
}
func (r *AutoscalingListenerReconciler) createRoleForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) {
newRole := r.ResourceBuilder.newScaleSetListenerRole(autoscalingListener)

View File

@@ -104,7 +104,7 @@ var _ = Describe("Test AutoScalingListener controller", func() {
})
Context("When creating a new AutoScalingListener", func() {
It("It should create/add all required resources for a new AutoScalingListener (finalizer, secret, service account, role, rolebinding, pod)", func() {
It("It should create/add all required resources for a new AutoScalingListener (finalizer, service account, role, rolebinding, config, pod)", func() {
config := new(corev1.Secret)
Eventually(
func() error {
@@ -134,19 +134,6 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListenerFinalizerName), "AutoScalingListener should have a finalizer")
// Check if secret is created
mirrorSecret := new(corev1.Secret)
Eventually(
func() (string, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerSecretMirrorName(autoscalingListener), Namespace: autoscalingListener.Namespace}, mirrorSecret)
if err != nil {
return "", err
}
return string(mirrorSecret.Data["github_token"]), nil
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListenerTestGitHubToken), "Mirror secret should be created")
// Check if service account is created
serviceAccount := new(corev1.ServiceAccount)
Eventually(
@@ -188,6 +175,22 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(scaleSetListenerRoleName(autoscalingListener)), "Rolebinding should be created")
listenerConfig := new(corev1.Secret)
Eventually(
func() error {
return k8sClient.Get(
ctx,
client.ObjectKey{
Name: scaleSetListenerConfigName(autoscalingListener),
Namespace: autoscalingListener.Namespace,
},
listenerConfig,
)
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval,
).Should(Succeed(), "Listener config should be created")
// Check if pod is created
pod := new(corev1.Pod)
Eventually(
@@ -397,75 +400,6 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingListenerTestInterval,
).ShouldNot(BeEquivalentTo(oldPodUID), "Pod should be re-created")
})
It("It should update mirror secrets to match secret used by AutoScalingRunnerSet", func() {
// Waiting for the pod is created
pod := new(corev1.Pod)
Eventually(
func() (string, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, pod)
if err != nil {
return "", err
}
return pod.Name, nil
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListener.Name), "Pod should be created")
// Update the secret
updatedSecret := configSecret.DeepCopy()
updatedSecret.Data["github_token"] = []byte(autoscalingListenerTestGitHubToken + "_updated")
err := k8sClient.Update(ctx, updatedSecret)
Expect(err).NotTo(HaveOccurred(), "failed to update test secret")
updatedPod := pod.DeepCopy()
// Ignore status running and consult the container state
updatedPod.Status.Phase = corev1.PodRunning
updatedPod.Status.ContainerStatuses = []corev1.ContainerStatus{
{
Name: autoscalingListenerContainerName,
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: 1,
},
},
},
}
err = k8sClient.Status().Update(ctx, updatedPod)
Expect(err).NotTo(HaveOccurred(), "failed to update test pod to failed")
// Check if mirror secret is updated with right data
mirrorSecret := new(corev1.Secret)
Eventually(
func() (map[string][]byte, error) {
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerSecretMirrorName(autoscalingListener), Namespace: autoscalingListener.Namespace}, mirrorSecret)
if err != nil {
return nil, err
}
return mirrorSecret.Data, nil
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(BeEquivalentTo(updatedSecret.Data), "Mirror secret should be updated")
// Check if we re-created a new pod
Eventually(
func() error {
latestPod := new(corev1.Pod)
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, latestPod)
if err != nil {
return err
}
if latestPod.UID == pod.UID {
return fmt.Errorf("Pod should be recreated")
}
return nil
},
autoscalingListenerTestTimeout,
autoscalingListenerTestInterval).Should(Succeed(), "Pod should be recreated")
})
})
})

View File

@@ -246,7 +246,7 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
// Our listener pod is out of date, so we need to delete it to get a new recreate.
listenerValuesHashChanged := listener.Annotations[annotationKeyValuesHash] != autoscalingRunnerSet.Annotations[annotationKeyValuesHash]
listenerSpecHashChanged := listener.Annotations[annotationKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash()
listenerSpecHashChanged := listener.Annotations[annotationKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash(secret)
if listenerFound && (listenerValuesHashChanged || listenerSpecHashChanged) {
log.Info("RunnerScaleSetListener is out of date. Deleting it so that it is recreated", "name", listener.Name)
if err := r.Delete(ctx, listener); err != nil {
@@ -297,7 +297,7 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
return ctrl.Result{}, nil
}
log.Info("Creating a new AutoscalingListener for the runner set", "ephemeralRunnerSetName", latestRunnerSet.Name)
return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log)
return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, secret, log)
}
// Update the status of autoscaling runner set.
@@ -643,7 +643,13 @@ func (r *AutoscalingRunnerSetReconciler) createEphemeralRunnerSet(ctx context.Co
return ctrl.Result{}, nil
}
func (r *AutoscalingRunnerSetReconciler) createAutoScalingListenerForRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, log logr.Logger) (ctrl.Result, error) {
func (r *AutoscalingRunnerSetReconciler) createAutoScalingListenerForRunnerSet(
ctx context.Context,
autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet,
ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet,
githubSecret *corev1.Secret,
log logr.Logger,
) (ctrl.Result, error) {
var imagePullSecrets []corev1.LocalObjectReference
for _, imagePullSecret := range r.DefaultRunnerScaleSetListenerImagePullSecrets {
imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{
@@ -651,7 +657,14 @@ func (r *AutoscalingRunnerSetReconciler) createAutoScalingListenerForRunnerSet(c
})
}
autoscalingListener, err := r.ResourceBuilder.newAutoScalingListener(autoscalingRunnerSet, ephemeralRunnerSet, r.ControllerNamespace, r.DefaultRunnerScaleSetListenerImage, imagePullSecrets)
autoscalingListener, err := r.ResourceBuilder.newAutoScalingListener(
autoscalingRunnerSet,
ephemeralRunnerSet,
githubSecret,
r.ControllerNamespace,
r.DefaultRunnerScaleSetListenerImage,
imagePullSecrets,
)
if err != nil {
log.Error(err, "Could not create AutoscalingListener spec")
return ctrl.Result{}, err

View File

@@ -476,6 +476,101 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
autoscalingRunnerSetTestInterval,
).Should(BeEquivalentTo("testgroup2"), "AutoScalingRunnerSet should have the runner group in its annotation")
})
It("should re-create the listener when the github secret changes", func() {
// Wait till the listener is created
listener := new(v1alpha1.AutoscalingListener)
Eventually(
func() error {
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(Succeed(), "Listener should be created")
actionsClient, err := controller.actionsClientFor(ctx, autoscalingRunnerSet)
Expect(err).NotTo(HaveOccurred(), "failed to get actions client")
listenerCreationTimestamp := listener.ObjectMeta.CreationTimestamp
listenerHash := listener.ObjectMeta.Annotations[annotationKeyRunnerSpecHash]
githubSecret := new(corev1.Secret)
Eventually(
func() error {
return k8sClient.Get(
ctx,
client.ObjectKey{
Name: configSecret.ObjectMeta.Name,
Namespace: configSecret.ObjectMeta.Namespace,
},
githubSecret,
)
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(Succeed(), "Failed to fetch the github secret")
githubSecret.Data["update"] = []byte("update")
err = k8sClient.Update(ctx, githubSecret)
Expect(err).NotTo(HaveOccurred(), "failed to update the github secret")
updatedGitHubSecret := new(corev1.Secret)
Eventually(
func() error {
err := k8sClient.Get(
ctx,
client.ObjectKey{
Name: configSecret.ObjectMeta.Name,
Namespace: configSecret.ObjectMeta.Namespace,
},
updatedGitHubSecret,
)
if err != nil {
return err
}
if _, ok := updatedGitHubSecret.Data["update"]; !ok {
return fmt.Errorf("secret update not yet present")
}
return nil
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(Succeed(), "Failed to eventually figure out github secret data update")
Eventually(
func() error {
updatedListener := new(v1alpha1.AutoscalingListener)
err := k8sClient.Get(
ctx,
client.ObjectKey{
Name: scaleSetListenerName(autoscalingRunnerSet),
Namespace: autoscalingRunnerSet.Namespace,
},
listener,
)
if err != nil {
return err
}
if updatedListener.CreationTimestamp == listenerCreationTimestamp {
return fmt.Errorf("creation timestamp not updated yet")
}
if updatedListener.Annotations[annotationKeyRunnerSpecHash] == listenerHash {
return fmt.Errorf("hash not updated yet")
}
return nil
},
autoscalingRunnerSetTestTimeout,
autoscalingRunnerSetTestInterval,
).Should(Succeed(), "Listener should be re-created")
actionsClientAfterUpdate, err := controller.actionsClientFor(ctx, autoscalingRunnerSet)
Expect(err).NotTo(HaveOccurred(), "failed to get actions client")
Expect(actionsClientAfterUpdate.(*fake.FakeClient).ID).NotTo(BeEquivalentTo(actionsClient.(*fake.FakeClient).ID), "expected new client to be used")
})
})
Context("When updating an AutoscalingRunnerSet with running or pending jobs", func() {

View File

@@ -78,7 +78,13 @@ func boolPtr(v bool) *bool {
return &v
}
func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, namespace, image string, imagePullSecrets []corev1.LocalObjectReference) (*v1alpha1.AutoscalingListener, error) {
func (b *ResourceBuilder) newAutoScalingListener(
autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet,
ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet,
githubSecret *corev1.Secret,
namespace, image string,
imagePullSecrets []corev1.LocalObjectReference,
) (*v1alpha1.AutoscalingListener, error) {
runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey])
if err != nil {
return nil, err
@@ -102,7 +108,7 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.
})
annotations := map[string]string{
annotationKeyRunnerSpecHash: autoscalingRunnerSet.ListenerSpecHash(),
annotationKeyRunnerSpecHash: autoscalingRunnerSet.ListenerSpecHash(githubSecret),
annotationKeyValuesHash: autoscalingRunnerSet.Annotations[annotationKeyValuesHash],
}

View File

@@ -59,7 +59,24 @@ func TestLabelPropagation(t *testing.T) {
assert.Equal(t, autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName], ephemeralRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName])
assert.Equal(t, autoscalingRunnerSet.Labels["arbitrary-label"], ephemeralRunnerSet.Labels["arbitrary-label"])
listener, err := b.newAutoScalingListener(&autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
githubSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scale-set",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("github_token"),
},
}
listener, err := b.newAutoScalingListener(
&autoscalingRunnerSet,
ephemeralRunnerSet,
githubSecret,
autoscalingRunnerSet.Namespace,
"test:latest",
nil,
)
require.NoError(t, err)
assert.Equal(t, labelValueKubernetesPartOf, listener.Labels[LabelKeyKubernetesPartOf])
assert.Equal(t, "runner-scale-set-listener", listener.Labels[LabelKeyKubernetesComponent])
@@ -120,6 +137,16 @@ func TestGitHubURLTrimLabelValues(t *testing.T) {
organization := strings.Repeat("b", 64)
repository := strings.Repeat("c", 64)
githubSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scale-set",
Namespace: "test-ns",
},
Data: map[string][]byte{
"github_token": []byte("github_token"),
},
}
autoscalingRunnerSet := v1alpha1.AutoscalingRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scale-set",
@@ -151,7 +178,14 @@ func TestGitHubURLTrimLabelValues(t *testing.T) {
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubOrganization], trimLabelVauleSuffix))
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubRepository], trimLabelVauleSuffix))
listener, err := b.newAutoScalingListener(autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
listener, err := b.newAutoScalingListener(
autoscalingRunnerSet,
ephemeralRunnerSet,
githubSecret,
autoscalingRunnerSet.Namespace,
"test:latest",
nil,
)
require.NoError(t, err)
assert.Len(t, listener.Labels[LabelKeyGitHubEnterprise], 0)
assert.Len(t, listener.Labels[LabelKeyGitHubOrganization], 63)
@@ -174,7 +208,14 @@ func TestGitHubURLTrimLabelValues(t *testing.T) {
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubOrganization], 0)
assert.Len(t, ephemeralRunnerSet.Labels[LabelKeyGitHubRepository], 0)
listener, err := b.newAutoScalingListener(autoscalingRunnerSet, ephemeralRunnerSet, autoscalingRunnerSet.Namespace, "test:latest", nil)
listener, err := b.newAutoScalingListener(
autoscalingRunnerSet,
ephemeralRunnerSet,
githubSecret,
autoscalingRunnerSet.Namespace,
"test:latest",
nil,
)
require.NoError(t, err)
assert.Len(t, listener.Labels[LabelKeyGitHubEnterprise], 63)
assert.True(t, strings.HasSuffix(ephemeralRunnerSet.Labels[LabelKeyGitHubEnterprise], trimLabelVauleSuffix))

View File

@@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"io"
"maps"
"math/rand"
"net/http"
"net/url"
@@ -273,16 +274,16 @@ func (c *Client) Identifier() string {
func (c *Client) Do(req *http.Request) (*http.Response, error) {
resp, err := c.Client.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("client request failed: %w", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read the response body: %w", err)
}
err = resp.Body.Close()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to close the response body: %w", err)
}
body = trimByteOrderMark(body)
@@ -294,7 +295,7 @@ func (c *Client) NewGitHubAPIRequest(ctx context.Context, method, path string, b
u := c.config.GitHubAPIURL(path)
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
}
req.Header.Set("User-Agent", c.userAgent.String())
@@ -305,28 +306,27 @@ func (c *Client) NewGitHubAPIRequest(ctx context.Context, method, path string, b
func (c *Client) NewActionsServiceRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
err := c.updateTokenIfNeeded(ctx)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue update token if needed: %w", err)
}
parsedPath, err := url.Parse(path)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to parse path %q: %w", path, err)
}
urlString, err := url.JoinPath(c.ActionsServiceURL, parsedPath.Path)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to join path (actions_service_url=%q, parsedPath=%q): %w", c.ActionsServiceURL, parsedPath.Path, err)
}
u, err := url.Parse(urlString)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to parse url string %q: %w", urlString, err)
}
q := u.Query()
for k, v := range parsedPath.Query() {
q[k] = v
}
maps.Copy(q, parsedPath.Query())
if q.Get("api-version") == "" {
q.Set("api-version", "6.0-preview")
}
@@ -334,7 +334,7 @@ func (c *Client) NewActionsServiceRequest(ctx context.Context, method, path stri
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new request with context: %w", err)
}
req.Header.Set("Content-Type", "application/json")
@@ -348,12 +348,12 @@ func (c *Client) GetRunnerScaleSet(ctx context.Context, runnerGroupId int, runne
path := fmt.Sprintf("/%s?runnerGroupId=%d&name=%s", scaleSetEndpoint, runnerGroupId, runnerScaleSetName)
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -386,12 +386,12 @@ func (c *Client) GetRunnerScaleSetById(ctx context.Context, runnerScaleSetId int
path := fmt.Sprintf("/%s/%d", scaleSetEndpoint, runnerScaleSetId)
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -413,12 +413,12 @@ func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (
path := fmt.Sprintf("/_apis/runtime/runnergroups/?groupName=%s", runnerGroup)
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -469,17 +469,17 @@ func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (
func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
body, err := json.Marshal(runnerScaleSet)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to marshal runner scale set: %w", err)
}
req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, scaleSetEndpoint, bytes.NewReader(body))
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -501,17 +501,17 @@ func (c *Client) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetId int,
body, err := json.Marshal(runnerScaleSet)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to marshal runner scale set: %w", err)
}
req, err := c.NewActionsServiceRequest(ctx, http.MethodPatch, path, bytes.NewReader(body))
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -533,12 +533,12 @@ func (c *Client) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetId int)
path := fmt.Sprintf("/%s/%d", scaleSetEndpoint, runnerScaleSetId)
req, err := c.NewActionsServiceRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return err
return fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return err
return fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusNoContent {
@@ -552,7 +552,7 @@ func (c *Client) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetId int)
func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, lastMessageId int64, maxCapacity int) (*RunnerScaleSetMessage, error) {
u, err := url.Parse(messageQueueUrl)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to parse message queue url: %w", err)
}
if lastMessageId > 0 {
@@ -567,7 +567,7 @@ func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAc
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new request with context: %w", err)
}
req.Header.Set("Accept", "application/json; api-version=6.0-preview")
@@ -577,7 +577,7 @@ func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAc
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode == http.StatusAccepted {
@@ -621,14 +621,14 @@ func (c *Client) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAc
func (c *Client) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error {
u, err := url.Parse(messageQueueUrl)
if err != nil {
return err
return fmt.Errorf("failed to parse message queue url: %w", err)
}
u.Path = fmt.Sprintf("%s/%d", u.Path, messageId)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u.String(), nil)
if err != nil {
return err
return fmt.Errorf("failed to create new request with context: %w", err)
}
req.Header.Set("Content-Type", "application/json")
@@ -637,7 +637,7 @@ func (c *Client) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueu
resp, err := c.Do(req)
if err != nil {
return err
return fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusNoContent {
@@ -673,14 +673,16 @@ func (c *Client) CreateMessageSession(ctx context.Context, runnerScaleSetId int,
requestData, err := json.Marshal(newSession)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to marshal new session: %w", err)
}
createdSession := &RunnerScaleSetSession{}
err = c.doSessionRequest(ctx, http.MethodPost, path, bytes.NewBuffer(requestData), http.StatusOK, createdSession)
if err = c.doSessionRequest(ctx, http.MethodPost, path, bytes.NewBuffer(requestData), http.StatusOK, createdSession); err != nil {
return nil, fmt.Errorf("failed to do the session request: %w", err)
}
return createdSession, err
return createdSession, nil
}
func (c *Client) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error {
@@ -691,19 +693,21 @@ func (c *Client) DeleteMessageSession(ctx context.Context, runnerScaleSetId int,
func (c *Client) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*RunnerScaleSetSession, error) {
path := fmt.Sprintf("/%s/%d/sessions/%s", scaleSetEndpoint, runnerScaleSetId, sessionId.String())
refreshedSession := &RunnerScaleSetSession{}
err := c.doSessionRequest(ctx, http.MethodPatch, path, nil, http.StatusOK, refreshedSession)
return refreshedSession, err
if err := c.doSessionRequest(ctx, http.MethodPatch, path, nil, http.StatusOK, refreshedSession); err != nil {
return nil, fmt.Errorf("failed to do the session request: %w", err)
}
return refreshedSession, nil
}
func (c *Client) doSessionRequest(ctx context.Context, method, path string, requestData io.Reader, expectedResponseStatusCode int, responseUnmarshalTarget any) error {
req, err := c.NewActionsServiceRequest(ctx, method, path, requestData)
if err != nil {
return err
return fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return err
return fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode == expectedResponseStatusCode {
@@ -749,12 +753,12 @@ func (c *Client) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQ
body, err := json.Marshal(requestIds)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to marshal request ids: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewBuffer(body))
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new request with context: %w", err)
}
req.Header.Set("Content-Type", "application/json")
@@ -763,7 +767,7 @@ func (c *Client) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQ
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -807,12 +811,12 @@ func (c *Client) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode == http.StatusNoContent {
@@ -842,17 +846,17 @@ func (c *Client) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *
body, err := json.Marshal(jitRunnerSetting)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to marshal runner settings: %w", err)
}
req, err := c.NewActionsServiceRequest(ctx, http.MethodPost, path, bytes.NewBuffer(body))
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -875,12 +879,12 @@ func (c *Client) GetRunner(ctx context.Context, runnerId int64) (*RunnerReferenc
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -904,12 +908,12 @@ func (c *Client) GetRunnerByName(ctx context.Context, runnerName string) (*Runne
req, err := c.NewActionsServiceRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -945,12 +949,12 @@ func (c *Client) RemoveRunner(ctx context.Context, runnerId int64) error {
req, err := c.NewActionsServiceRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return err
return fmt.Errorf("failed to create new actions service request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return err
return fmt.Errorf("failed to issue the request: %w", err)
}
if resp.StatusCode != http.StatusNoContent {
@@ -969,13 +973,13 @@ type registrationToken struct {
func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationToken, error) {
path, err := createRegistrationTokenPath(c.config)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create registration token path: %w", err)
}
var buf bytes.Buffer
req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, &buf)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
}
bearerToken := ""
@@ -985,7 +989,7 @@ func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationT
} else {
accessToken, err := c.fetchAccessToken(ctx, c.config.ConfigURL.String(), c.creds.AppCreds)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to fetch access token: %w", err)
}
bearerToken = fmt.Sprintf("Bearer %v", accessToken.Token)
@@ -998,14 +1002,14 @@ func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationT
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read the body: %w", err)
}
return nil, &GitHubAPIError{
StatusCode: resp.StatusCode,
@@ -1035,13 +1039,13 @@ type accessToken struct {
func (c *Client) fetchAccessToken(ctx context.Context, gitHubConfigURL string, creds *GitHubAppAuth) (*accessToken, error) {
accessTokenJWT, err := createJWTForGitHubApp(creds)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create JWT for GitHub app: %w", err)
}
path := fmt.Sprintf("/app/installations/%v/access_tokens", creds.AppInstallationID)
req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
}
req.Header.Set("Content-Type", "application/vnd.github+json")
@@ -1051,7 +1055,7 @@ func (c *Client) fetchAccessToken(ctx context.Context, gitHubConfigURL string, c
resp, err := c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
defer resp.Body.Close()
@@ -1096,12 +1100,12 @@ func (c *Client) getActionsServiceAdminConnection(ctx context.Context, rt *regis
enc.SetEscapeHTML(false)
if err := enc.Encode(body); err != nil {
return nil, err
return nil, fmt.Errorf("failed to encode body: %w", err)
}
req, err := c.NewGitHubAPIRequest(ctx, http.MethodPost, path, buf)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new GitHub API request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
@@ -1115,7 +1119,7 @@ func (c *Client) getActionsServiceAdminConnection(ctx context.Context, rt *regis
var err error
resp, err = c.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
defer resp.Body.Close()
@@ -1215,7 +1219,7 @@ func createJWTForGitHubApp(appAuth *GitHubAppAuth) (string, error) {
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(appAuth.AppPrivateKey))
if err != nil {
return "", err
return "", fmt.Errorf("failed to parse RSA private key from PEM: %w", err)
}
return token.SignedString(privateKey)

View File

@@ -101,8 +101,7 @@ func TestCreateMessageSession(t *testing.T) {
err,
)
gotErr := err.(*actions.ActionsError)
assert.Equal(t, want, gotErr)
assert.Equal(t, want, errorTypeForComparison)
})
t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) {

View File

@@ -123,6 +123,7 @@ var defaultRunnerScaleSetJitRunnerConfig = &actions.RunnerScaleSetJitRunnerConfi
// FakeClient implements actions service
type FakeClient struct {
id uuid.UUID
getRunnerScaleSetResult struct {
*actions.RunnerScaleSet
err error
@@ -191,7 +192,9 @@ type FakeClient struct {
}
func NewFakeClient(options ...Option) actions.ActionsService {
f := &FakeClient{}
f := &FakeClient{
id: uuid.New(),
}
f.applyDefaults()
for _, opt := range options {
opt(f)
@@ -199,6 +202,10 @@ func NewFakeClient(options ...Option) actions.ActionsService {
return f
}
func (f *FakeClient) ID() uuid.UUID {
return f.id
}
func (f *FakeClient) applyDefaults() {
f.getRunnerScaleSetResult.RunnerScaleSet = defaultRunnerScaleSet
f.getRunnerScaleSetByIdResult.RunnerScaleSet = defaultRunnerScaleSet

32
go.mod
View File

@@ -17,23 +17,23 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/kelseyhightower/envconfig v1.4.0
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.23.0
github.com/onsi/gomega v1.36.2
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/prometheus/client_golang v1.21.1
github.com/stretchr/testify v1.10.0
github.com/teambition/rrule-go v1.8.2
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.37.0
golang.org/x/net v0.39.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0
golang.org/x/sync v0.13.0
gomodules.xyz/jsonpatch/v2 v2.5.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.32.2
k8s.io/apimachinery v0.32.2
k8s.io/client-go v0.32.2
k8s.io/api v0.32.3
k8s.io/apimachinery v0.32.3
k8s.io/client-go v0.32.3
k8s.io/utils v0.0.0-20241210054802-24370beab758
sigs.k8s.io/controller-runtime v0.20.3
sigs.k8s.io/controller-runtime v0.20.4
sigs.k8s.io/yaml v1.4.0
)
@@ -93,6 +93,7 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-sql-driver/mysql v1.9.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -107,7 +108,7 @@ require (
github.com/google/go-github/v69 v69.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gruntwork-io/go-commons v0.17.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -134,6 +135,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.4.0 // indirect
@@ -149,16 +151,18 @@ require (
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/crypto v0.36.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.32.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect

53
go.sum
View File

@@ -127,6 +127,7 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
@@ -177,6 +178,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -258,17 +261,22 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.23.0 h1:FA1xjp8ieYDzlgS5ABTpdUDB7wtngggONc8a7ku2NqQ=
github.com/onsi/ginkgo/v2 v2.23.0/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -315,6 +323,8 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -326,6 +336,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -336,8 +348,10 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -346,6 +360,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -358,12 +374,18 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -373,6 +395,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -396,6 +420,7 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -405,22 +430,22 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4=
k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9 h1:t0huyHnz6HsokckRxAF1bY0cqPFwzINKCL7yltEjZQc=
k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI=
sigs.k8s.io/controller-runtime v0.20.3/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=

View File

@@ -49,3 +49,22 @@ func ComputeTemplateHash(template interface{}) string {
return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32()))
}
func ComputeCombinedObjectsHash(first any, others ...any) string {
hasher := fnv.New32a()
hasher.Reset()
printer := spew.ConfigState{
Indent: " ",
SortKeys: true,
DisableMethods: true,
SpewKeys: true,
}
for _, obj := range append([]any{first}, others...) {
printer.Fprintf(hasher, "%#v", obj)
}
return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32()))
}