1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package componentcore
import (
"context"
"errors"
"fmt"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
"github.com/johnandersen777/socialweb-computer-kcp/internal/app/compose"
"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/kube"
)
const RestartFieldManager = "socialweb.computer/dev-restart"
type DynamicFactory = kube.Factory
type componentKind struct {
Kind string
GVR schema.GroupVersionResource
}
var runtimeKinds = []componentKind{
{compose.ComponentKindBidder, compose.BidderGVR},
{compose.ComponentKindGuestControlPlane, compose.GuestControlPlaneGVR},
{compose.ComponentKindPds, compose.PdsGVR},
{compose.ComponentKindRequester, compose.RequesterGVR},
{compose.ComponentKindGatewayAPI, compose.GatewayAPIGVR},
{compose.ComponentKindSshServer, compose.SshServerGVR},
{compose.ComponentKindPlc, compose.PlcGVR},
{compose.ComponentKindRelay, compose.RelayGVR},
{compose.ComponentKindIngressProxy, compose.IngressProxyGVR},
{compose.ComponentKindDispatcherTLS, compose.DispatcherTLSGVR},
}
type ComponentRef struct {
Workspace string
Kind string
Name string
GVR schema.GroupVersionResource
}
type RestartStatus struct {
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Phase string `json:"phase,omitempty"`
Ready bool `json:"ready"`
Message string `json:"message,omitempty"`
Restarts int32 `json:"restarts,omitempty"`
StartedAt string `json:"startedAt,omitempty"`
Endpoints []v1alpha1.Endpoint `json:"endpoints,omitempty"`
}
func FindComponents(ctx context.Context, dynamic DynamicFactory, workspaces []string, kind, name string) ([]ComponentRef, error) {
var out []ComponentRef
for _, workspace := range workspaces {
dyn, err := dynamic(workspace)
if err != nil {
return nil, err
}
for _, candidate := range runtimeKinds {
if kind != "" && candidate.Kind != kind {
continue
}
u, err := kube.Get[metav1.PartialObjectMetadata](ctx, dyn.Resource(candidate.GVR), name)
if apierrors.IsNotFound(err) || apierrors.IsForbidden(err) {
continue
}
if err != nil {
return nil, fmt.Errorf("componentcore: reading %s %s in %s: %w", candidate.Kind, name, workspace, err)
}
out = append(out, ComponentRef{Workspace: workspace, Kind: candidate.Kind, Name: u.GetName(), GVR: candidate.GVR})
}
}
return out, nil
}
func DescribeComponents(ctx context.Context, dynamic DynamicFactory, workspaces []string) []string {
var out []string
for _, workspace := range workspaces {
dyn, err := dynamic(workspace)
if err != nil {
continue
}
for _, kind := range runtimeKinds {
list, err := kube.List[metav1.PartialObjectMetadata](ctx, dyn.Resource(kind.GVR), metav1.ListOptions{})
if err != nil {
continue
}
for i := range list {
out = append(out, workspace+":"+kind.Kind+"/"+list[i].GetName())
}
}
}
return out
}
func ReadStatus(ctx context.Context, dynamic DynamicFactory, ref ComponentRef) (RestartStatus, error) {
dyn, err := dynamic(ref.Workspace)
if err != nil {
return RestartStatus{}, err
}
u, err := kube.Get[componentStatus](ctx, dyn.Resource(ref.GVR), ref.Name)
if err != nil {
return RestartStatus{}, err
}
return u.Status, nil
}
func Restart(ctx context.Context, dynamic DynamicFactory, ref ComponentRef) (string, error) {
dyn, err := dynamic(ref.Workspace)
if err != nil {
return "", err
}
stamp := time.Now().UTC().Format(time.RFC3339Nano)
body := kube.Doc{"apiVersion": ref.GVR.GroupVersion().String(), "kind": ref.Kind,
"metadata": map[string]any{"name": ref.Name}, "spec": map[string]any{"restartedAt": stamp}}
if _, err := kube.Apply[kube.Doc](ctx, dyn.Resource(ref.GVR), body,
kube.ApplyOptions{FieldManager: RestartFieldManager, Force: true}); err != nil {
return "", fmt.Errorf("componentcore: requesting the restart of %s/%s: %w", ref.Kind, ref.Name, err)
}
return stamp, nil
}
func WaitRestarted(ctx context.Context, dynamic DynamicFactory, ref ComponentRef, stamp string, before RestartStatus, timeout time.Duration) (RestartStatus, error) {
requested, err := time.Parse(time.RFC3339Nano, stamp)
if err != nil {
return RestartStatus{}, err
}
dyn, err := dynamic(ref.Workspace)
if err != nil {
return RestartStatus{}, err
}
last := before
_, err = kube.Await(ctx, dyn.Resource(ref.GVR), ref.Name, timeout, func(u *componentStatus) (bool, error) {
if u == nil {
return false, nil
}
last = u.Status
return last.Ready && restartedAfter(last, before, requested), nil
})
if errors.Is(err, kube.ErrAwaitTimeout) {
return last, fmt.Errorf("componentcore: %s/%s in %s did not restart within %s (phase %s, ready %v, restarts %d, startedAt %s)",
ref.Kind, ref.Name, ref.Workspace, timeout, last.Phase, last.Ready, last.Restarts, last.StartedAt)
}
return last, err
}
func restartedAfter(cur, before RestartStatus, requested time.Time) bool {
if cur.StartedAt != "" {
if started, err := time.Parse(time.RFC3339, cur.StartedAt); err == nil {
if !started.Before(requested.Truncate(time.Second)) {
return true
}
}
}
if cur.Restarts > before.Restarts {
return true
}
return false
}
type componentStatus struct {
Status RestartStatus `json:"status"`
}