Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/shim/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
ContainerNamesAnnotationKey = "zeropod.ctrox.dev/container-names"
ScaleDownDurationAnnotationKey = "zeropod.ctrox.dev/scaledown-duration"
DisableCheckpoiningAnnotationKey = "zeropod.ctrox.dev/disable-checkpointing"
DryRunAnnotationKey = "zeropod.ctrox.dev/dry-run"
PreDumpAnnotationKey = "zeropod.ctrox.dev/pre-dump"
MigrateAnnotationKey = "zeropod.ctrox.dev/migrate"
LiveMigrateAnnotationKey = "zeropod.ctrox.dev/live-migrate"
Expand Down Expand Up @@ -60,6 +61,7 @@ var ContainerdAnnotations = []string{
ContainerNamesAnnotationKey,
ScaleDownDurationAnnotationKey,
DisableCheckpoiningAnnotationKey,
DryRunAnnotationKey,
PreDumpAnnotationKey,
MigrateAnnotationKey,
LiveMigrateAnnotationKey,
Expand All @@ -76,6 +78,7 @@ type AnnotationConfig struct {
Ports []uint16
ScaleDownDuration time.Duration
DisableCheckpointing bool
DryRun bool
PreDump bool
Migrate []string
LiveMigrate string
Expand Down Expand Up @@ -175,6 +178,20 @@ func NewConfig(ctx context.Context, spec *specs.Spec) (*Config, error) {
migrate = strings.Split(migrateValue, containersDelim)
}

dryRunValue := spec.Annotations[DryRunAnnotationKey]
dryRun := false
if dryRunValue != "" {
dryRun, err = strconv.ParseBool(dryRunValue)
if err != nil {
return nil, err
}
}

liveMigrateValue := spec.Annotations[LiveMigrateAnnotationKey]
if dryRun && (slices.Contains(migrate, containerName) || (liveMigrateValue != "" && liveMigrateValue == containerName)) {
return nil, fmt.Errorf("dry-run (%s) cannot be combined with migrate/live-migrate for container %q", DryRunAnnotationKey, containerName)
}

ns, ok := namespaces.Namespace(ctx)
if !ok {
ns = defaultContainerdNS
Expand Down Expand Up @@ -243,6 +260,7 @@ func NewConfig(ctx context.Context, spec *specs.Spec) (*Config, error) {
Ports: containerPorts,
ScaleDownDuration: dur,
DisableCheckpointing: disableCheckpointing,
DryRun: dryRun,
PreDump: preDump,
Migrate: migrate,
LiveMigrate: spec.Annotations[LiveMigrateAnnotationKey],
Expand Down
25 changes: 25 additions & 0 deletions api/shim/v1/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ func TestNewConfig(t *testing.T) {
assert.False(t, cfg.DisableCheckpointing)
},
},
"dry run": {
annotations: map[string]string{
DryRunAnnotationKey: "true",
},
assertCfg: func(t *testing.T, cfg *Config) {
assert.True(t, cfg.DryRun)
},
},
"dry run default false": {
annotations: map[string]string{},
assertCfg: func(t *testing.T, cfg *Config) {
assert.False(t, cfg.DryRun)
},
},
"predump": {
annotations: map[string]string{
PreDumpAnnotationKey: "true",
Expand Down Expand Up @@ -119,3 +133,14 @@ func TestNewConfig(t *testing.T) {
})
}
}

func TestNewConfigDryRunMigrateConflict(t *testing.T) {
_, err := NewConfig(context.Background(), &specs.Spec{
Annotations: map[string]string{
CRIContainerNameAnnotation: "app",
DryRunAnnotationKey: "true",
MigrateAnnotationKey: "app",
},
})
require.Error(t, err)
}
33 changes: 30 additions & 3 deletions api/shim/v1/shim.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions api/shim/v1/shim.proto
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ message ContainerStatus {
google.protobuf.Timestamp event_time = 6;
google.protobuf.Duration event_duration = 7;
string event_log = 8;
bool dry_run = 9;
}

message ContainerMetrics {
Expand All @@ -59,4 +60,6 @@ message ContainerMetrics {
bool running = 8;
int64 checkpoint_errors = 9;
int64 restore_errors = 10;
int64 dry_run_scale_downs = 11;
int64 dry_run_would_restores = 12;
}
31 changes: 31 additions & 0 deletions config/examples/nginx-dry-run.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-dry-run
spec:
replicas: 1
selector:
matchLabels:
app: nginx-dry-run
template:
metadata:
labels:
app: nginx-dry-run
annotations:
zeropod.ctrox.dev/scaledown-duration: 15s
zeropod.ctrox.dev/dry-run: "true"
spec:
runtimeClassName: zeropod
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
livenessProbe:
periodSeconds: 1
httpGet:
port: 80
resources:
requests:
cpu: 100m
memory: 128Mi
19 changes: 19 additions & 0 deletions docs/configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,22 @@ been established. Defaults to `5s` if unset.
Features that are marked as experimental might change form in the future or
could be removed entirely in future releases depending on the stability and
need.

### Dry Run

```yaml
zeropod.ctrox.dev/dry-run: "true"
```

Runs the scale-down timer exactly as normal (same activity tracking, same
scale-down duration logic) but never actually checkpoints or kills the
process and never enables traffic redirection - the container keeps running
and serving traffic uninterrupted. Instead, zeropod logs when it *would have*
scaled down and when it *would have* restored (also emitted as a Kubernetes
event on the pod, and counted in the `zeropod_dry_run_scale_downs_total`/
`zeropod_dry_run_would_restores_total` metrics - see [metrics](../metrics.md)).
Use this to evaluate zeropod's behaviour on a workload risk-free before
enabling it for real. Cannot be combined with `migrate`/`live-migrate`. Note
that in-place resource scaling (`cpu-requests`/`memory-requests`) is also not
previewed - since the container is never actually marked as scaled down, its
resource requests are never touched while dry-run is active.
6 changes: 6 additions & 0 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ zeropod_checkpoint_errors_total{container="nginx",namespace="default",pod="nginx
# HELP zeropod_restore_errors_total Total number of restore errors.
# TYPE zeropod_restore_errors_total counter
zeropod_restore_errors_total{container="nginx",namespace="default",pod="nginx"} 0
# HELP zeropod_dry_run_scale_downs_total Total number of simulated dry-run scale downs.
# TYPE zeropod_dry_run_scale_downs_total counter
zeropod_dry_run_scale_downs_total{container="nginx",namespace="default",pod="nginx"} 0
# HELP zeropod_dry_run_would_restores_total Total number of simulated dry-run restores.
# TYPE zeropod_dry_run_would_restores_total counter
zeropod_dry_run_would_restores_total{container="nginx",namespace="default",pod="nginx"} 0
```
7 changes: 7 additions & 0 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ func TestE2E(t *testing.T) {
maxReqDuration: time.Second,
expectRunning: true,
},
"pod with dry-run": {
pod: testPod(dryRun(true), defaultScaleDownAfter),
parallelReqs: 1,
sequentialReqs: 1,
maxReqDuration: time.Second,
expectRunning: true,
},
"pod with multiple containers": {
pod: testPod(agnContainer("c1", 8080), agnContainer("c2", 8081), defaultScaleDownAfter),
svc: testService(8081),
Expand Down
6 changes: 6 additions & 0 deletions e2e/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@ func disableCheckpointing(disable bool) podOption {
})
}

func dryRun(dryRun bool) podOption {
return annotations(map[string]string{
shimv1.DryRunAnnotationKey: strconv.FormatBool(dryRun),
})
}

func scaleDownAfter(dur time.Duration) podOption {
return annotations(map[string]string{
shimv1.ScaleDownDurationAnnotationKey: dur.String(),
Expand Down
14 changes: 9 additions & 5 deletions manager/event_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
reasonScaledDown = "Scaled down"
reasonCheckpointFailed = "Checkpoint failed"
reasonRestoreFailed = "Restore failed"
reasonDryRun = "Dry run"
)

type EventCreator struct {
Expand All @@ -44,24 +45,27 @@ func (ec *EventCreator) Handle(ctx context.Context, status *v1.ContainerStatus,
clog.Info("status event")

message, reason := "", ""
switch status.Phase {
case v1.ContainerPhase_RUNNING:
switch {
case status.DryRun:
reason = reasonDryRun
message = fmt.Sprintf("Dry-run: container %s %s", status.Name, status.EventLog)
case status.Phase == v1.ContainerPhase_RUNNING:
reason = reasonRunning
// don't create an event if container was simply started without being restored
if status.EventDuration == nil || status.EventDuration.AsDuration() == 0 {
return nil
}
message = fmt.Sprintf("Restored container %s in %s", status.Name, status.EventDuration.AsDuration())
case v1.ContainerPhase_SCALED_DOWN:
case status.Phase == v1.ContainerPhase_SCALED_DOWN:
reason = reasonScaledDown
message = fmt.Sprintf("Scaled down container %s", status.Name)
if status.EventDuration != nil {
message += " in " + status.EventDuration.AsDuration().String()
}
case v1.ContainerPhase_CHECKPOINT_FAILED:
case status.Phase == v1.ContainerPhase_CHECKPOINT_FAILED:
reason = reasonCheckpointFailed
message = fmt.Sprintf("Checkpoint failed for container %s", status.Name)
case v1.ContainerPhase_RESTORE_FAILED:
case status.Phase == v1.ContainerPhase_RESTORE_FAILED:
reason = reasonRestoreFailed
message = fmt.Sprintf("Restore failed for container %s", status.Name)
}
Expand Down
23 changes: 23 additions & 0 deletions manager/event_creator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func TestEventCreator(t *testing.T) {
for name, tc := range map[string]struct {
statusPhase v1.ContainerPhase
statusDuration time.Duration
statusDryRun bool
statusEventLog string
containerName string
expectedReason string
expectedMessage string
Expand Down Expand Up @@ -58,6 +60,25 @@ func TestEventCreator(t *testing.T) {
expectedReason: reasonScaledDown,
expectedMessage: "Scaled down container c in 1s",
},
"dry run scaled down": {
containerName: "c",
// dry-run never changes the real phase, unlike a real scale down.
statusPhase: v1.ContainerPhase_RUNNING,
statusDryRun: true,
statusEventLog: "would have scaled down after 10s of inactivity",
expectEventCreated: true,
expectedReason: reasonDryRun,
expectedMessage: "Dry-run: container c would have scaled down after 10s of inactivity",
},
"dry run restored": {
containerName: "c",
statusPhase: v1.ContainerPhase_RUNNING,
statusDryRun: true,
statusEventLog: "would have restored (got exec)",
expectEventCreated: true,
expectedReason: reasonDryRun,
expectedMessage: "Dry-run: container c would have restored (got exec)",
},
} {
t.Run(name, func(t *testing.T) {
client := fake.NewClientBuilder().WithScheme(scheme).Build()
Expand All @@ -71,6 +92,8 @@ func TestEventCreator(t *testing.T) {
PodName: pod.Name,
PodNamespace: pod.Namespace,
Phase: tc.statusPhase,
DryRun: tc.statusDryRun,
EventLog: tc.statusEventLog,
}
if tc.statusDuration != 0 {
status.EventDuration = durationpb.New(tc.statusDuration)
Expand Down
Loading
Loading