@@ -23,6 +23,7 @@ import (
2323 "net"
2424 "os"
2525 "path/filepath"
26+ "sync/atomic"
2627 "testing"
2728 "time"
2829
@@ -39,6 +40,7 @@ const testWorkerToken = "test-worker-token"
3940
4041type mockNVCFServer struct {
4142 pb.UnimplementedWorkerServer
43+ requestSecretCredentials func (context.Context , * pb.SecretCredentialsRequest ) (* pb.SecretCredentialsResponse , error )
4244}
4345
4446func (s * mockNVCFServer ) ConnectOnce (_ context.Context , _ * pb.WorkerConnect ) (* pb.WorkerConnectOnceResponse , error ) {
@@ -49,19 +51,65 @@ func (s *mockNVCFServer) ConnectOnce(_ context.Context, _ *pb.WorkerConnect) (*p
4951 }, nil
5052}
5153
54+ func (s * mockNVCFServer ) RequestSecretCredentials (
55+ ctx context.Context ,
56+ req * pb.SecretCredentialsRequest ,
57+ ) (* pb.SecretCredentialsResponse , error ) {
58+ if s .requestSecretCredentials == nil {
59+ return s .UnimplementedWorkerServer .RequestSecretCredentials (ctx , req )
60+ }
61+ return s .requestSecretCredentials (ctx , req )
62+ }
63+
5264func startMockNVCFServer (t * testing.T ) string {
65+ return startMockNVCFServerWithImplementation (t , & mockNVCFServer {})
66+ }
67+
68+ func startMockNVCFServerWithImplementation (t * testing.T , implementation * mockNVCFServer ) string {
5369 t .Helper ()
54- lis , err := net .Listen ("tcp" , "127.0.0.1:0" )
70+ var listenConfig net.ListenConfig
71+ lis , err := listenConfig .Listen (context .Background (), "tcp" , "127.0.0.1:0" )
5572 if err != nil {
5673 t .Fatalf ("failed to listen: %v" , err )
5774 }
5875 srv := grpc .NewServer ()
59- pb .RegisterWorkerServer (srv , & mockNVCFServer {})
60- go srv .Serve (lis )
61- t .Cleanup (srv .GracefulStop )
76+ pb .RegisterWorkerServer (srv , implementation )
77+ serveErr := make (chan error , 1 )
78+ go func () {
79+ serveErr <- srv .Serve (lis )
80+ }()
81+ t .Cleanup (func () {
82+ srv .GracefulStop ()
83+ if err := <- serveErr ; err != nil {
84+ t .Errorf ("serve mock NVCF server: %v" , err )
85+ }
86+ })
6287 return fmt .Sprintf ("http://%s" , lis .Addr ().String ())
6388}
6489
90+ func waitForFileContent (path , want string , timeout time.Duration ) bool {
91+ deadline := time .Now ().Add (timeout )
92+ for time .Now ().Before (deadline ) {
93+ content , err := os .ReadFile (path )
94+ if err == nil && string (content ) == want {
95+ return true
96+ }
97+ time .Sleep (10 * time .Millisecond )
98+ }
99+ content , err := os .ReadFile (path )
100+ return err == nil && string (content ) == want
101+ }
102+
103+ func cancelAndWaitForRun (t * testing.T , cancel context.CancelFunc , runErr <- chan error ) {
104+ t .Helper ()
105+ cancel ()
106+ select {
107+ case <- runErr :
108+ case <- time .After (2 * time .Second ):
109+ t .Log ("Run did not stop within the cleanup timeout" )
110+ }
111+ }
112+
65113func TestRun_WritesTokenToDisk (t * testing.T ) {
66114 addr := startMockNVCFServer (t )
67115 tmpDir := t .TempDir ()
@@ -110,3 +158,134 @@ func TestRun_WritesTokenToDisk(t *testing.T) {
110158 t .Fatalf ("expected token %q, got %q" , testWorkerToken , string (content ))
111159 }
112160}
161+
162+ func TestRun_RefreshesESSAssertionTokenUntilCancelled (t * testing.T ) {
163+ tmpDir := t .TempDir ()
164+ assertionTokenPath := filepath .Join (tmpDir , "ess" , "jwt.token" )
165+ if err := os .MkdirAll (filepath .Dir (assertionTokenPath ), 0755 ); err != nil {
166+ t .Fatalf ("create assertion token directory: %v" , err )
167+ }
168+ if err := os .WriteFile (assertionTokenPath , []byte ("worker-init-token" ), 0600 ); err != nil {
169+ t .Fatalf ("write initial assertion token: %v" , err )
170+ }
171+
172+ var requestCount atomic.Int32
173+ addr := startMockNVCFServerWithImplementation (t , & mockNVCFServer {
174+ requestSecretCredentials : func (
175+ _ context.Context ,
176+ _ * pb.SecretCredentialsRequest ,
177+ ) (* pb.SecretCredentialsResponse , error ) {
178+ call := requestCount .Add (1 )
179+ tokenValue := "refreshed-assertion-1"
180+ expiration := time .Now ().Add (300 * time .Millisecond )
181+ if call > 1 {
182+ tokenValue = "refreshed-assertion-2"
183+ expiration = time .Now ().Add (time .Hour )
184+ }
185+ return & pb.SecretCredentialsResponse {
186+ SecretCredentialsToken : tokenValue ,
187+ Expiration : timestamppb .New (expiration ),
188+ }, nil
189+ },
190+ })
191+
192+ cfg := configs.Config {
193+ NvcfFqdnGrpc : addr ,
194+ NvcfWorkerToken : "initial-token" ,
195+ FunctionId : "test-function-id" ,
196+ FunctionVersionId : "test-function-version-id" ,
197+ NcaId : "test-nca-id" ,
198+ InstanceId : "test-instance-id" ,
199+ SharedConfigDir : tmpDir ,
200+ WorkerTokenPath : filepath .Join (tmpDir , "worker-token" ),
201+ ESSAssertionTokenPath : assertionTokenPath ,
202+ }
203+
204+ w , err := New (cfg )
205+ if err != nil {
206+ t .Fatalf ("New: %v" , err )
207+ }
208+
209+ ctx , cancel := context .WithCancel (context .Background ())
210+ runErr := lo .Async (func () error { return w .Run (ctx ) })
211+
212+ if ! waitForFileContent (assertionTokenPath , "refreshed-assertion-1" , 5 * time .Second ) {
213+ cancelAndWaitForRun (t , cancel , runErr )
214+ t .Fatal ("existing assertion token was not replaced by the immediate refresh" )
215+ }
216+ info , err := os .Stat (assertionTokenPath )
217+ if err != nil {
218+ cancelAndWaitForRun (t , cancel , runErr )
219+ t .Fatalf ("stat refreshed assertion token: %v" , err )
220+ }
221+ if got := info .Mode ().Perm (); got != 0644 {
222+ cancelAndWaitForRun (t , cancel , runErr )
223+ t .Fatalf ("assertion token mode = %o, want 0644" , got )
224+ }
225+ if ! waitForFileContent (assertionTokenPath , "refreshed-assertion-2" , 5 * time .Second ) {
226+ cancelAndWaitForRun (t , cancel , runErr )
227+ t .Fatal ("assertion token was not rotated on the next refresh cycle" )
228+ }
229+
230+ cancel ()
231+ select {
232+ case err := <- runErr :
233+ if err != nil {
234+ t .Fatalf ("Run after cancellation: %v" , err )
235+ }
236+ case <- time .After (2 * time .Second ):
237+ t .Fatal ("Run did not stop cleanly after cancellation" )
238+ }
239+ }
240+
241+ func TestRun_WithoutESSAssertionTokenPathSkipsSecretCredentialRefresh (t * testing.T ) {
242+ var requestCount atomic.Int32
243+ addr := startMockNVCFServerWithImplementation (t , & mockNVCFServer {
244+ requestSecretCredentials : func (
245+ _ context.Context ,
246+ _ * pb.SecretCredentialsRequest ,
247+ ) (* pb.SecretCredentialsResponse , error ) {
248+ requestCount .Add (1 )
249+ return & pb.SecretCredentialsResponse {
250+ SecretCredentialsToken : "unexpected-token" ,
251+ Expiration : timestamppb .New (time .Now ().Add (time .Hour )),
252+ }, nil
253+ },
254+ })
255+
256+ tmpDir := t .TempDir ()
257+ assertionTokenPath := filepath .Join (tmpDir , "ess" , "jwt.token" )
258+ workerTokenPath := filepath .Join (tmpDir , "worker-token" )
259+ cfg := configs.Config {
260+ NvcfFqdnGrpc : addr ,
261+ NvcfWorkerToken : "initial-token" ,
262+ FunctionId : "test-function-id" ,
263+ FunctionVersionId : "test-function-version-id" ,
264+ NcaId : "test-nca-id" ,
265+ InstanceId : "test-instance-id" ,
266+ SharedConfigDir : tmpDir ,
267+ WorkerTokenPath : workerTokenPath ,
268+ }
269+
270+ w , err := New (cfg )
271+ if err != nil {
272+ t .Fatalf ("New: %v" , err )
273+ }
274+ ctx , cancel := context .WithCancel (context .Background ())
275+ runErr := lo .Async (func () error { return w .Run (ctx ) })
276+ if ! waitForFileContent (workerTokenPath , testWorkerToken , 5 * time .Second ) {
277+ cancelAndWaitForRun (t , cancel , runErr )
278+ t .Fatal ("worker token was not written" )
279+ }
280+ cancel ()
281+ if err := <- runErr ; err != nil {
282+ t .Fatalf ("Run: %v" , err )
283+ }
284+
285+ if got := requestCount .Load (); got != 0 {
286+ t .Fatalf ("RequestSecretCredentials calls = %d, want 0" , got )
287+ }
288+ if _ , err := os .Stat (assertionTokenPath ); ! os .IsNotExist (err ) {
289+ t .Fatalf ("assertion token file should not exist without ESS_ASSERTION_TOKEN_PATH, stat error: %v" , err )
290+ }
291+ }
0 commit comments