Skip to content

Commit c1c96c6

Browse files
committed
Make exporter lastseen timeout configurable
1 parent c680017 commit c1c96c6

File tree

8 files changed

+196
-26
lines changed

8 files changed

+196
-26
lines changed

.github/workflows/e2e.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ jobs:
1919
controller-ref: ${{ github.ref }}
2020
# use the matching branch on the jumpstarter repo
2121
jumpstarter-ref: ${{ github.event.pull_request.base.ref }}
22-
e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e:
22+
# test the current controller with the previous version of python and E2E tests
23+
# to ensure backwards compatibility
24+
e2e-tests-release-0-7:
2325
runs-on: ubuntu-latest
2426
steps:
25-
- uses: jumpstarter-dev/jumpstarter-e2e@11a5ce6734be9f089ec3ea6ebf55284616f67fe8
27+
- uses: jumpstarter-dev/jumpstarter-e2e@release-0.7
2628
with:
2729
controller-ref: ${{ github.ref }}
28-
jumpstarter-ref: 28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
30+
jumpstarter-ref: release-0.7

cmd/main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func main() {
154154
os.Exit(1)
155155
}
156156

157-
authenticator, prefix, router, option, provisioning, err := config.LoadConfiguration(
157+
authenticator, prefix, router, option, provisioning, exporterOptions, err := config.LoadConfiguration(
158158
context.Background(),
159159
mgr.GetAPIReader(),
160160
mgr.GetScheme(),
@@ -174,9 +174,10 @@ func main() {
174174
}
175175

176176
if err = (&controller.ExporterReconciler{
177-
Client: mgr.GetClient(),
178-
Scheme: mgr.GetScheme(),
179-
Signer: oidcSigner,
177+
Client: mgr.GetClient(),
178+
Scheme: mgr.GetScheme(),
179+
Signer: oidcSigner,
180+
ExporterOptions: *exporterOptions,
180181
}).SetupWithManager(mgr); err != nil {
181182
setupLog.Error(err, "unable to create controller", "controller", "Exporter")
182183
os.Exit(1)

deploy/helm/jumpstarter/charts/jumpstarter-controller/model.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ class JWTAuthenticator(BaseModel):
203203
userValidationRules: Optional[List[UserValidationRule]] = None
204204

205205

206+
class ExporterOptions(BaseModel):
207+
model_config = ConfigDict(extra="forbid")
208+
209+
offlineTimeout: Optional[str] = Field(
210+
None,
211+
description="How long to wait before marking the exporter as offline",
212+
)
213+
214+
206215
class Authentication(BaseModel):
207216
model_config = ConfigDict(extra="forbid")
208217

@@ -219,6 +228,7 @@ class JumpstarterConfig(BaseModel):
219228
provisioning: Optional[Provisioning] = None
220229
authentication: Optional[Authentication] = None
221230
grpc: Optional[Grpc] = None
231+
exporterOptions: Optional[ExporterOptions] = None
222232

223233

224234
class Nodeport(BaseModel):

deploy/helm/jumpstarter/charts/jumpstarter-controller/values.schema.json

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,26 @@
214214
"title": "ClaimValidationRule",
215215
"type": "object"
216216
},
217+
"ExporterOptions": {
218+
"additionalProperties": false,
219+
"properties": {
220+
"offlineTimeout": {
221+
"anyOf": [
222+
{
223+
"type": "string"
224+
},
225+
{
226+
"type": "null"
227+
}
228+
],
229+
"default": null,
230+
"description": "How long to wait before marking the exporter as offline",
231+
"title": "Offlinetimeout"
232+
}
233+
},
234+
"title": "ExporterOptions",
235+
"type": "object"
236+
},
217237
"ExtraItem": {
218238
"additionalProperties": false,
219239
"properties": {
@@ -638,6 +658,17 @@
638658
}
639659
],
640660
"default": null
661+
},
662+
"exporterOptions": {
663+
"anyOf": [
664+
{
665+
"$ref": "#/$defs/ExporterOptions"
666+
},
667+
{
668+
"type": "null"
669+
}
670+
],
671+
"default": null
641672
}
642673
},
643674
"title": "JumpstarterConfig",
@@ -671,6 +702,71 @@
671702
"default": null,
672703
"description": "Whether to allow keepalive pings even when there are no active streams(RPCs)",
673704
"title": "Permitwithoutstream"
705+
},
706+
"timeout": {
707+
"anyOf": [
708+
{
709+
"type": "string"
710+
},
711+
{
712+
"type": "null"
713+
}
714+
],
715+
"default": null,
716+
"description": "How long the server waits for a ping response before closing the connection",
717+
"title": "Timeout"
718+
},
719+
"maxConnectionIdle": {
720+
"anyOf": [
721+
{
722+
"type": "string"
723+
},
724+
{
725+
"type": "null"
726+
}
727+
],
728+
"default": null,
729+
"description": "Maximum time a connection can be idle before being closed",
730+
"title": "Maxconnectionidle"
731+
},
732+
"maxConnectionAge": {
733+
"anyOf": [
734+
{
735+
"type": "string"
736+
},
737+
{
738+
"type": "null"
739+
}
740+
],
741+
"default": null,
742+
"description": "Maximum lifetime of a connection before it's closed",
743+
"title": "Maxconnectionage"
744+
},
745+
"maxConnectionAgeGrace": {
746+
"anyOf": [
747+
{
748+
"type": "string"
749+
},
750+
{
751+
"type": "null"
752+
}
753+
],
754+
"default": null,
755+
"description": "Grace period after max connection age before forcible closure",
756+
"title": "Maxconnectionagegrace"
757+
},
758+
"time": {
759+
"anyOf": [
760+
{
761+
"type": "string"
762+
},
763+
{
764+
"type": "null"
765+
}
766+
],
767+
"default": null,
768+
"description": "How often the server sends keepalive pings to clients",
769+
"title": "Time"
674770
}
675771
},
676772
"title": "Keepalive",

deploy/helm/jumpstarter/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ global:
4242
## @param jumpstarter-controller.config.authentication.internal.prefix. Prefix to add to the subject claim of the tokens issued by the builtin authenticator.
4343
## @param jumpstarter-controller.config.authentication.jwt. External OIDC authentication, see https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-authentication-configuration for documentation
4444

45+
## @param jumpstarter-controller.config.exporterOptions.offlineTimeout. How long to wait before marking the exporter as offline.
46+
4547
## @section Ingress And Route parameters
4648
## @descriptionStart This section contains parameters for the Ingress and Route configurations.
4749
## You can enable either the gRPC ingress or the OpenShift route but not both.
@@ -77,6 +79,9 @@ jumpstarter-controller:
7779
namespace: ""
7880

7981
config:
82+
exporterOptions:
83+
offlineTimeout: 180s # how long to wait before marking the exporter as offline
84+
8085
grpc:
8186
keepalive:
8287
# EnforcementPolicy parameters

internal/config/config.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,30 @@ func LoadConfiguration(
4949
key client.ObjectKey,
5050
signer *oidc.Signer,
5151
certificateAuthority string,
52-
) (authenticator.Token, string, Router, []grpc.ServerOption, *Provisioning, error) {
52+
) (authenticator.Token, string, Router, []grpc.ServerOption, *Provisioning, *ExporterOptions, error) {
5353
var configmap corev1.ConfigMap
5454
if err := client.Get(ctx, key, &configmap); err != nil {
55-
return nil, "", nil, nil, nil, err
55+
return nil, "", nil, nil, nil, nil, err
5656
}
5757

5858
rawRouter, ok := configmap.Data["router"]
5959
if !ok {
60-
return nil, "", nil, nil, nil, fmt.Errorf("LoadConfiguration: missing router section")
60+
return nil, "", nil, nil, nil, nil, fmt.Errorf("LoadConfiguration: missing router section")
6161
}
6262

6363
var router Router
6464
if err := yaml.Unmarshal([]byte(rawRouter), &router); err != nil {
65-
return nil, "", nil, nil, nil, err
65+
return nil, "", nil, nil, nil, nil, err
6666
}
6767

6868
rawConfig, ok := configmap.Data["config"]
6969
if !ok {
70-
return nil, "", nil, nil, nil, fmt.Errorf("LoadConfiguration: missing config section")
70+
return nil, "", nil, nil, nil, nil, fmt.Errorf("LoadConfiguration: missing config section")
7171
}
7272

7373
var config Config
7474
if err := yaml.UnmarshalStrict([]byte(rawConfig), &config); err != nil {
75-
return nil, "", nil, nil, nil, err
75+
return nil, "", nil, nil, nil, nil, err
7676
}
7777

7878
authenticator, prefix, err := LoadAuthenticationConfiguration(
@@ -83,13 +83,16 @@ func LoadConfiguration(
8383
certificateAuthority,
8484
)
8585
if err != nil {
86-
return nil, "", nil, nil, nil, err
86+
return nil, "", nil, nil, nil, nil, err
8787
}
8888

8989
serverOptions, err := LoadGrpcConfiguration(config.Grpc)
9090
if err != nil {
91-
return nil, "", nil, nil, nil, err
91+
return nil, "", nil, nil, nil, nil, err
9292
}
9393

94-
return authenticator, prefix, router, serverOptions, &config.Provisioning, nil
94+
// Preprocess configuration values (parse durations, cache expensive operations, etc.)
95+
config.ExporterOptions.PreprocessConfig()
96+
97+
return authenticator, prefix, router, serverOptions, &config.Provisioning, &config.ExporterOptions, nil
9598
}

internal/config/types.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package config
22

33
import (
4+
"time"
5+
46
apiserverv1beta1 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1"
57
)
68

79
type Config struct {
8-
Authentication Authentication `json:"authentication"`
9-
Provisioning Provisioning `json:"provisioning"`
10-
Grpc Grpc `json:"grpc"`
10+
Authentication Authentication `json:"authentication"`
11+
Provisioning Provisioning `json:"provisioning"`
12+
Grpc Grpc `json:"grpc"`
13+
ExporterOptions ExporterOptions `json:"exporterOptions"`
1114
}
1215

1316
type Authentication struct {
@@ -40,6 +43,36 @@ type Keepalive struct {
4043
Time string `json:"time,omitempty"` // How often server sends pings
4144
}
4245

46+
type ExporterOptions struct {
47+
OfflineTimeout string `json:"offlineTimeout,omitempty"` // How long to wait before marking the exporter as offline
48+
offlineTimeoutDur time.Duration // Pre-calculated duration, set during LoadConfiguration
49+
}
50+
51+
// PreprocessConfig parses and caches configuration values that require processing
52+
// This method should be called once during configuration loading to pre-calculate
53+
// expensive operations and cache the results for efficient retrieval
54+
func (e *ExporterOptions) PreprocessConfig() {
55+
// Parse and cache the offline timeout duration
56+
if e.OfflineTimeout == "" {
57+
e.offlineTimeoutDur = time.Minute // Default fallback
58+
} else {
59+
duration, err := time.ParseDuration(e.OfflineTimeout)
60+
if err != nil {
61+
// If parsing fails, use default fallback
62+
e.offlineTimeoutDur = time.Minute
63+
} else {
64+
e.offlineTimeoutDur = duration
65+
}
66+
}
67+
68+
// Future configuration parsing can be added here
69+
}
70+
71+
// GetOfflineTimeout returns the pre-calculated offline timeout duration
72+
func (e *ExporterOptions) GetOfflineTimeout() time.Duration {
73+
return e.offlineTimeoutDur
74+
}
75+
4376
type Router map[string]RouterEntry
4477

4578
type RouterEntry struct {

internal/controller/exporter_controller.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ import (
3030
"sigs.k8s.io/controller-runtime/pkg/log"
3131

3232
jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
33+
"github.com/jumpstarter-dev/jumpstarter-controller/internal/config"
3334
"github.com/jumpstarter-dev/jumpstarter-controller/internal/oidc"
3435
)
3536

3637
// ExporterReconciler reconciles a Exporter object
3738
type ExporterReconciler struct {
3839
client.Client
39-
Scheme *runtime.Scheme
40-
Signer *oidc.Signer
40+
Scheme *runtime.Scheme
41+
Signer *oidc.Signer
42+
ExporterOptions config.ExporterOptions
4143
}
4244

4345
// +kubebuilder:rbac:groups=jumpstarter.dev,resources=exporters,verbs=get;list;watch;create;update;patch;delete
@@ -158,6 +160,7 @@ func (r *ExporterReconciler) reconcileStatusConditionsOnline(
158160
exporter *jumpstarterdevv1alpha1.Exporter,
159161
) (ctrl.Result, error) {
160162
var requeueAfter time.Duration = 0
163+
offlineTimeout := r.ExporterOptions.GetOfflineTimeout()
161164

162165
if exporter.Status.LastSeen.IsZero() {
163166
meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{
@@ -168,13 +171,13 @@ func (r *ExporterReconciler) reconcileStatusConditionsOnline(
168171
Message: "Never seen",
169172
})
170173
// marking the exporter offline, no need to requeue
171-
} else if time.Since(exporter.Status.LastSeen.Time) > time.Minute {
174+
} else if time.Since(exporter.Status.LastSeen.Time) > offlineTimeout {
172175
meta.SetStatusCondition(&exporter.Status.Conditions, metav1.Condition{
173176
Type: string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline),
174177
Status: metav1.ConditionFalse,
175178
ObservedGeneration: exporter.Generation,
176179
Reason: "Seen",
177-
Message: "Last seen more than 1 minute ago",
180+
Message: fmt.Sprintf("Last seen more than %v ago", offlineTimeout),
178181
})
179182
// marking the exporter offline, no need to requeue
180183
} else {
@@ -183,10 +186,27 @@ func (r *ExporterReconciler) reconcileStatusConditionsOnline(
183186
Status: metav1.ConditionTrue,
184187
ObservedGeneration: exporter.Generation,
185188
Reason: "Seen",
186-
Message: "Last seen less than 1 minute ago",
189+
Message: fmt.Sprintf("Last seen less than %v ago", offlineTimeout),
187190
})
188-
// marking the exporter online, requeue after 30 seconds
189-
requeueAfter = time.Second * 30
191+
192+
// Calculate when the exporter will go offline
193+
expirationTime := exporter.Status.LastSeen.Add(offlineTimeout)
194+
timeUntilExpiration := time.Until(expirationTime)
195+
196+
// Set requeue time to be just after expiration (with a small safety margin)
197+
// This way we can definitively determine if the exporter expired or was updated
198+
safetyMargin := 10 * time.Second
199+
if timeUntilExpiration > 0 {
200+
// Exporter hasn't expired yet, requeue after expiration + safety margin
201+
requeueAfter = timeUntilExpiration + safetyMargin
202+
// Cap the requeue time to avoid very long waits
203+
if requeueAfter > 5*time.Minute {
204+
requeueAfter = 5 * time.Minute
205+
}
206+
} else {
207+
// Exporter should have already expired, requeue in safety margin time
208+
requeueAfter = safetyMargin
209+
}
190210
}
191211

192212
if exporter.Status.Devices == nil {

0 commit comments

Comments
 (0)