@@ -16,6 +16,7 @@ import (
1616 "github.com/samber/lo"
1717 "github.com/spf13/cobra"
1818 "google.golang.org/grpc/status"
19+ "google.golang.org/protobuf/types/known/timestamppb"
1920 "tailscale.com/types/key"
2021)
2122
@@ -58,6 +59,18 @@ func init() {
5859 }
5960 nodeCmd .AddCommand (expireNodeCmd )
6061
62+ extendNodeExpirationCmd .Flags ().Uint64P ("identifier" , "i" , 0 , "Node identifier (ID)" )
63+ err = extendNodeExpirationCmd .MarkFlagRequired ("identifier" )
64+ if err != nil {
65+ log .Fatal (err .Error ())
66+ }
67+ extendNodeExpirationCmd .Flags ().StringP ("new-expiry" , "e" , "" , "New expiration time in RFC3339 format, e.g., 2024-01-01T15:04:05Z" )
68+ err = extendNodeExpirationCmd .MarkFlagRequired ("new-expiry" )
69+ if err != nil {
70+ log .Fatal (err .Error ())
71+ }
72+ nodeCmd .AddCommand (extendNodeExpirationCmd )
73+
6174 renameNodeCmd .Flags ().Uint64P ("identifier" , "i" , 0 , "Node identifier (ID)" )
6275 err = renameNodeCmd .MarkFlagRequired ("identifier" )
6376 if err != nil {
@@ -320,6 +333,54 @@ var expireNodeCmd = &cobra.Command{
320333 },
321334}
322335
336+ var extendNodeExpirationCmd = & cobra.Command {
337+ Use : "extend-expiration" ,
338+ Short : "Extends the expiration of a node by setting a new expiration time" ,
339+ Run : func (cmd * cobra.Command , args []string ) {
340+ output , _ := cmd .Flags ().GetString ("output" )
341+ nodeID , err := cmd .Flags ().GetUint64 ("identifier" )
342+ if err != nil {
343+ ErrorOutput (err , fmt .Sprintf ("Error getting identifier from flag: %s" , err ), output )
344+ }
345+
346+ newExpiryStr , err := cmd .Flags ().GetString ("new-expiry" )
347+ if err != nil {
348+ ErrorOutput (err , fmt .Sprintf ("Error getting new-expiry from flag: %s" , err ), output )
349+ }
350+
351+ // Parse the new-expiry timestamp from string to time.Time
352+ newExpiry , err := time .Parse (time .RFC3339 , newExpiryStr )
353+ if err != nil {
354+ ErrorOutput (err , fmt .Sprintf ("Invalid expiration time format: must be in RFC3339 format, %s" , err ), output )
355+ }
356+
357+ // Set up context and gRPC client
358+ ctx , client , conn , cancel := newHeadscaleCLIWithConfig ()
359+ defer cancel ()
360+ defer conn .Close ()
361+
362+ // Build the gRPC request
363+ request := & v1.ExtendNodeExpirationRequest {
364+ NodeId : nodeID ,
365+ NewExpiration : timestamppb .New (newExpiry ), // convert time.Time to protobuf timestamp
366+ }
367+
368+ // Make the gRPC call
369+ response , err := client .ExtendNodeExpiration (ctx , request )
370+ if err != nil {
371+ st , ok := status .FromError (err )
372+ if ok {
373+ ErrorOutput (st .Err (), st .Message (), output )
374+ } else {
375+ ErrorOutput (err , "Unexpected error during ExtendNodeExpiration RPC call" , output )
376+ }
377+ }
378+
379+ // Print the result
380+ SuccessOutput (response , "Node expiration extended successfully" , output )
381+ },
382+ }
383+
323384var renameNodeCmd = & cobra.Command {
324385 Use : "rename NEW_NAME" ,
325386 Short : "Renames a node in your network" ,
0 commit comments