Skip to content

Commit 7839ea5

Browse files
committed
add min ticker
1 parent 4842da9 commit 7839ea5

File tree

18 files changed

+1378
-47
lines changed

18 files changed

+1378
-47
lines changed

apps/workspace-engine/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ require (
126126
github.com/sourcegraph/conc v0.3.0 // indirect
127127
github.com/spf13/afero v1.12.0 // indirect
128128
github.com/spf13/cast v1.7.1 // indirect
129+
github.com/stretchr/objx v0.5.2 // indirect
129130
github.com/subosito/gotenv v1.6.0 // indirect
130131
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
131132
go.uber.org/atomic v1.9.0 // indirect

apps/workspace-engine/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
469469
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
470470
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
471471
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
472+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
473+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
472474
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
473475
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
474476
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=

apps/workspace-engine/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"workspace-engine/pkg/kafka"
1313
"workspace-engine/pkg/server"
14+
"workspace-engine/pkg/ticker"
1415

1516
"github.com/charmbracelet/log"
1617
"github.com/spf13/pflag"
@@ -136,6 +137,22 @@ func main() {
136137
ctx, cancel := context.WithCancel(ctx)
137138
defer cancel()
138139

140+
// Initialize Kafka producer for ticker
141+
producer, err := kafka.NewProducer()
142+
if err != nil {
143+
log.Fatal("Failed to create Kafka producer", "error", err)
144+
}
145+
defer producer.Close()
146+
147+
// Start periodic ticker for time-sensitive policy evaluation
148+
workspaceTicker := ticker.New(producer)
149+
go func() {
150+
log.Info("Workspace ticker started")
151+
if err := workspaceTicker.Run(ctx); err != nil {
152+
log.Error("Ticker error", "error", err)
153+
}
154+
}()
155+
139156
go func() {
140157
log.Info("Kafka consumer started")
141158
if err := kafka.RunConsumer(ctx); err != nil {

apps/workspace-engine/oapi/openapi.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,51 @@
242242
],
243243
"type": "object"
244244
},
245+
"EnvironmentProgressionRule": {
246+
"properties": {
247+
"dependsOnEnvironmentSelector": {
248+
"$ref": "#/components/schemas/Selector"
249+
},
250+
"id": {
251+
"type": "string"
252+
},
253+
"maximumAgeHours": {
254+
"description": "Maximum age of dependency deployment before blocking progression (prevents stale promotions)",
255+
"format": "int32",
256+
"minimum": 0,
257+
"type": "integer"
258+
},
259+
"minimumSockTimeMinutes": {
260+
"default": 0,
261+
"description": "Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed",
262+
"format": "int32",
263+
"minimum": 0,
264+
"type": "integer"
265+
},
266+
"minimumSuccessPercentage": {
267+
"default": 100,
268+
"format": "float",
269+
"maximum": 100,
270+
"minimum": 0,
271+
"type": "number"
272+
},
273+
"policyId": {
274+
"type": "string"
275+
},
276+
"successStatuses": {
277+
"items": {
278+
"$ref": "#/components/schemas/JobStatus"
279+
},
280+
"type": "array"
281+
}
282+
},
283+
"required": [
284+
"id",
285+
"policyId",
286+
"dependsOnEnvironmentSelector"
287+
],
288+
"type": "object"
289+
},
245290
"ErrorResponse": {
246291
"properties": {
247292
"error": {
@@ -550,6 +595,9 @@
550595
"createdAt": {
551596
"type": "string"
552597
},
598+
"environmentProgression": {
599+
"$ref": "#/components/schemas/EnvironmentProgressionRule"
600+
},
553601
"id": {
554602
"type": "string"
555603
},

apps/workspace-engine/oapi/spec/schemas/policy.jsonnet

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,35 @@ local openapi = import '../lib/openapi.libsonnet';
4040
policyId: { type: 'string' },
4141
createdAt: { type: 'string' },
4242
anyApproval: openapi.schemaRef('AnyApprovalRule'),
43+
environmentProgression: openapi.schemaRef('EnvironmentProgressionRule'),
44+
},
45+
},
46+
47+
EnvironmentProgressionRule: {
48+
type: 'object',
49+
required: ['id', 'policyId', 'dependsOnEnvironmentSelector'],
50+
properties: {
51+
id: { type: 'string' },
52+
policyId: { type: 'string' },
53+
dependsOnEnvironmentSelector: openapi.schemaRef('Selector'),
54+
55+
minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 },
56+
successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') },
57+
58+
minimumSockTimeMinutes: {
59+
type: 'integer',
60+
format: 'int32',
61+
minimum: 0,
62+
default: 0,
63+
description: 'Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed',
64+
},
65+
66+
maximumAgeHours: {
67+
type: 'integer',
68+
format: 'int32',
69+
minimum: 0,
70+
description: 'Maximum age of dependency deployment before blocking progression (prevents stale promotions)',
71+
},
4372
},
4473
},
4574

apps/workspace-engine/pkg/events/events.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"workspace-engine/pkg/events/handler/resources"
1515
"workspace-engine/pkg/events/handler/resourcevariables"
1616
"workspace-engine/pkg/events/handler/system"
17+
"workspace-engine/pkg/events/handler/tick"
1718
"workspace-engine/pkg/events/handler/userapprovalrecords"
1819
)
1920

@@ -71,6 +72,8 @@ var handlers = handler.HandlerRegistry{
7172
handler.GithubEntityCreate: githubentities.HandleGithubEntityCreated,
7273
handler.GithubEntityUpdate: githubentities.HandleGithubEntityUpdated,
7374
handler.GithubEntityDelete: githubentities.HandleGithubEntityDeleted,
75+
76+
handler.WorkspaceTick: tick.HandleWorkspaceTick,
7477
}
7578

7679
func NewEventHandler() *handler.EventListener {

apps/workspace-engine/pkg/events/handler/handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ const (
7474
GithubEntityCreate EventType = "github-entity.created"
7575
GithubEntityUpdate EventType = "github-entity.updated"
7676
GithubEntityDelete EventType = "github-entity.deleted"
77+
78+
WorkspaceTick EventType = "workspace.tick"
7779
)
7880

7981
// RawEvent represents the raw event data received from Kafka messages
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package tick
2+
3+
import (
4+
"context"
5+
6+
"workspace-engine/pkg/changeset"
7+
"workspace-engine/pkg/events/handler"
8+
"workspace-engine/pkg/workspace"
9+
10+
"github.com/charmbracelet/log"
11+
"go.opentelemetry.io/otel"
12+
"go.opentelemetry.io/otel/attribute"
13+
"go.opentelemetry.io/otel/codes"
14+
)
15+
16+
var tracer = otel.Tracer("events/handler/tick")
17+
18+
// HandleWorkspaceTick handles periodic workspace tick events by marking all release targets
19+
// as tainted to trigger re-evaluation. This is needed for time-sensitive policies like:
20+
// - RRule deployment windows (time-based allow/deny windows)
21+
// - Environment progression soak time (wait N minutes after deployment)
22+
// - Environment progression maximum age (deployments become too old)
23+
func HandleWorkspaceTick(ctx context.Context, ws *workspace.Workspace, event handler.RawEvent) error {
24+
ctx, span := tracer.Start(ctx, "HandleWorkspaceTick")
25+
defer span.End()
26+
27+
span.SetAttributes(
28+
attribute.String("workspace.id", ws.ID),
29+
attribute.String("event.type", string(event.EventType)),
30+
)
31+
32+
changeSet, ok := changeset.FromContext[any](ctx)
33+
if !ok {
34+
span.SetStatus(codes.Error, "changeset not found in context")
35+
return nil
36+
}
37+
38+
releaseTargets, err := ws.ReleaseTargets().Items(ctx)
39+
if err != nil {
40+
span.RecordError(err)
41+
span.SetStatus(codes.Error, "failed to get release targets")
42+
return err
43+
}
44+
45+
// Mark all release targets as tainted to trigger re-evaluation
46+
taintedCount := 0
47+
for _, rt := range releaseTargets {
48+
changeSet.Record(changeset.ChangeTypeTaint, rt)
49+
taintedCount++
50+
}
51+
52+
span.SetAttributes(attribute.Int("release_targets.tainted", taintedCount))
53+
54+
log.Debug("Workspace tick processed",
55+
"workspaceID", ws.ID,
56+
"tainted_count", taintedCount)
57+
58+
span.SetStatus(codes.Ok, "tick processed")
59+
return nil
60+
}

apps/workspace-engine/pkg/events/handler/update.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
// mergeFields creates a new entity by copying target and updating specified fields from source
99
func MergeFields[T any](target, source *T, fieldsToUpdate []string) (*T, error) {
1010
result := new(T)
11-
11+
1212
// Copy all fields from target to result
1313
targetValue := reflect.ValueOf(target).Elem()
1414
resultValue := reflect.ValueOf(result).Elem()
@@ -40,13 +40,13 @@ func MergeFields[T any](target, source *T, fieldsToUpdate []string) (*T, error)
4040
// Includes both the struct field name and JSON tag name
4141
func buildFieldMap(t reflect.Type) map[string]int {
4242
fieldMap := make(map[string]int)
43-
43+
4444
for i := 0; i < t.NumField(); i++ {
4545
field := t.Field(i)
46-
46+
4747
// Add struct field name
4848
fieldMap[field.Name] = i
49-
49+
5050
// Add JSON tag name if present
5151
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
5252
jsonName := strings.Split(jsonTag, ",")[0]
@@ -55,8 +55,6 @@ func buildFieldMap(t reflect.Type) map[string]int {
5555
}
5656
}
5757
}
58-
58+
5959
return fieldMap
6060
}
61-
62-

apps/workspace-engine/pkg/events/handler/update_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ func TestMergeFields_ZeroValues(t *testing.T) {
274274
}
275275

276276
source := &TestEntity{
277-
Name: "", // Zero value for string
278-
Count: 0, // Zero value for int
279-
Active: false, // Zero value for bool
277+
Name: "", // Zero value for string
278+
Count: 0, // Zero value for int
279+
Active: false, // Zero value for bool
280280
}
281281

282282
// Update with zero values - should update to zero values
@@ -359,7 +359,7 @@ func TestMergeFields_DoesNotModifyOriginal(t *testing.T) {
359359
func TestBuildFieldMap(t *testing.T) {
360360
entity := TestEntity{}
361361
entityType := reflect.TypeOf(entity)
362-
362+
363363
fieldMap := buildFieldMap(entityType)
364364

365365
// Check struct field names
@@ -400,4 +400,3 @@ func TestBuildFieldMap(t *testing.T) {
400400
t.Errorf("Expected 'Name' and 'name' to map to same index, got %d and %d", nameIndex, jsonNameIndex)
401401
}
402402
}
403-

0 commit comments

Comments
 (0)