Skip to content

Commit 6e14dfe

Browse files
fix: Check if token is expired (#49)
* Check if token is over the refresh interval * Add tests
1 parent af4439d commit 6e14dfe

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

controllers/gitrepository_controller.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"context"
55
"fmt"
66
"path/filepath"
7-
"sigs.k8s.io/controller-runtime/pkg/manager"
87
"strings"
98
"time"
109

10+
"sigs.k8s.io/controller-runtime/pkg/manager"
11+
1112
"github.com/go-logr/logr"
1213
apierrors "k8s.io/apimachinery/pkg/api/errors"
1314
"k8s.io/apimachinery/pkg/api/meta"
@@ -97,6 +98,23 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques
9798
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
9899
}
99100

101+
// Check if existing secret has a valid token
102+
existingSecret, err := r.secretManager.GetSecret(ctx, secretNamespace, secretName)
103+
if err == nil && r.secretManager.IsSecretManagedByController(existingSecret) {
104+
expiry, err := r.secretManager.GetTokenExpiry(existingSecret)
105+
if err == nil {
106+
refreshThreshold := r.Config.TokenRefresh.RefreshInterval
107+
if time.Until(expiry) > refreshThreshold {
108+
logger.V(1).Info("Token still valid, skipping regeneration", "expiresAt", expiry)
109+
// Schedule token refresh as usual
110+
if err := r.refreshManager.ScheduleRefresh(ctx, secretNamespace, secretName, gitRepo.Spec.URL); err != nil {
111+
logger.Error(err, "Failed to schedule token refresh")
112+
}
113+
return ctrl.Result{RequeueAfter: time.Until(expiry) - 5*time.Minute}, nil
114+
}
115+
}
116+
}
117+
100118
// Generate GitHub installation token
101119
installationToken, err := r.githubClient.GenerateInstallationToken(ctx, gitRepo.Spec.URL)
102120
if err != nil {

controllers/gitrepository_controller_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,3 +854,97 @@ func TestIsTargetOrganizationRepository(t *testing.T) {
854854
})
855855
}
856856
}
857+
858+
func TestGitRepositoryReconciler_Reconcile_SkipsRegenerationIfTokenValid(t *testing.T) {
859+
s := scheme.Scheme
860+
require.NoError(t, sourcev1.AddToScheme(s))
861+
862+
// Set up a valid token expiry in the future
863+
expiresAt := time.Now().Add(1 * time.Hour)
864+
refreshInterval := 30 * time.Minute
865+
866+
gitRepo := &sourcev1.GitRepository{
867+
ObjectMeta: metav1.ObjectMeta{
868+
Name: "test-repo",
869+
Namespace: "default",
870+
},
871+
Spec: sourcev1.GitRepositorySpec{
872+
URL: "https://github.com/testorg/test-repository",
873+
SecretRef: &meta.LocalObjectReference{
874+
Name: "test-secret",
875+
},
876+
},
877+
}
878+
879+
secret := &corev1.Secret{
880+
ObjectMeta: metav1.ObjectMeta{
881+
Name: "test-secret",
882+
Namespace: "default",
883+
Annotations: map[string]string{
884+
kubernetes.AnnotationManagedBy: "flux-extension-controller",
885+
kubernetes.AnnotationTokenExpiry: expiresAt.Format(time.RFC3339),
886+
kubernetes.AnnotationRepositoryURL: "https://github.com/testorg/test-repository",
887+
},
888+
},
889+
Data: map[string][]byte{
890+
"username": []byte("git"),
891+
"password": []byte("existing-token"),
892+
},
893+
Type: kubernetes.SecretTypeGitRepository,
894+
}
895+
896+
fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(gitRepo, secret).Build()
897+
898+
cfg := &config.Config{
899+
GitHub: config.GitHubConfig{
900+
Organization: "testorg",
901+
},
902+
Controller: config.ControllerConfig{
903+
ExcludedNamespaces: []string{"flux-system"},
904+
},
905+
TokenRefresh: config.TokenRefreshConfig{
906+
RefreshInterval: refreshInterval,
907+
},
908+
}
909+
910+
mockGitHubClient := &MockGitHubClient{}
911+
// ValidateRepositoryURL should be called, but GenerateInstallationToken should NOT be called
912+
mockGitHubClient.On("ValidateRepositoryURL", "https://github.com/testorg/test-repository").Return(nil)
913+
914+
mockRefreshManager := &MockRefreshManager{}
915+
mockRefreshManager.On("ScheduleRefresh", mock.Anything, "default", "test-secret", "https://github.com/testorg/test-repository").Return(nil)
916+
917+
reconciler := &GitRepositoryReconciler{
918+
Client: fakeClient,
919+
Scheme: s,
920+
Config: cfg,
921+
githubClient: mockGitHubClient,
922+
secretManager: kubernetes.NewSecretManager(fakeClient),
923+
refreshManager: mockRefreshManager,
924+
logger: logr.Discard(),
925+
}
926+
927+
ctx := context.Background()
928+
req := reconcile.Request{
929+
NamespacedName: types.NamespacedName{
930+
Name: "test-repo",
931+
Namespace: "default",
932+
},
933+
}
934+
935+
result, err := reconciler.Reconcile(ctx, req)
936+
require.NoError(t, err)
937+
// Should requeue just before expiry minus 5 minutes
938+
expectedRequeue := time.Until(expiresAt) - 5*time.Minute
939+
assert.InDelta(t, expectedRequeue.Seconds(), result.RequeueAfter.Seconds(), 2.0) // allow 2s drift
940+
941+
// Secret should not be changed
942+
var updatedSecret corev1.Secret
943+
err = fakeClient.Get(ctx, types.NamespacedName{Name: "test-secret", Namespace: "default"}, &updatedSecret)
944+
require.NoError(t, err)
945+
assert.Equal(t, []byte("existing-token"), updatedSecret.Data["password"])
946+
947+
// GenerateInstallationToken should NOT be called
948+
mockGitHubClient.AssertNotCalled(t, "GenerateInstallationToken", mock.Anything, mock.Anything)
949+
mockRefreshManager.AssertExpectations(t)
950+
}

0 commit comments

Comments
 (0)