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
package cloudprovider
import (
"context"
"errors"
"fmt"
corev1 "k8s.io/api/core/v1"
)
type NodeWriter interface {
PatchProviderID(ctx context.Context, nodeName, providerID string) error
SetLabels(ctx context.Context, nodeName string, set map[string]string, remove []string) error
DeleteNode(ctx context.Context, nodeName string) error
}
type Options struct {
Cluster string
Instances Instances
Nodes NodeWriter
Log func(event string, fields map[string]any)
}
// ponytail: this is a hand-rolled subset of the Kubernetes cloud-provider node contracts -- node
// init (providerID and labels) and node lifecycle (delete on gone or draining) only. The ceiling is
// that it is not a cloudprovider.Interface, so it can serve no zone, route or load balancer. The
// upgrade path is k8s.io/cloud-provider/app with a cloudprovider.Interface once a load balancer or a
// zone concept exists here.
type Reconciler struct {
cluster string
instances Instances
nodes NodeWriter
log func(event string, fields map[string]any)
}
func NewReconciler(o Options) (*Reconciler, error) {
if o.Cluster == "" {
return nil, errors.New(
"cloudprovider: the controller needs the name of the cluster whose Nodes it owns; an " +
"empty one would stamp provider IDs and labels that name no cluster")
}
if o.Instances == nil {
return nil, errors.New(
"cloudprovider: the controller has no instance source, so every Node would read as " +
"unknown and be deleted")
}
if o.Nodes == nil {
return nil, errors.New(
"cloudprovider: the controller has no Node writer, so every decision would be made " +
"and dropped")
}
log := o.Log
if log == nil {
log = func(string, map[string]any) {}
}
return &Reconciler{
cluster: o.Cluster,
instances: o.Instances,
nodes: o.Nodes,
log: log,
}, nil
}
func (r *Reconciler) Cluster() string {
return r.cluster
}
func (r *Reconciler) Reconcile(ctx context.Context, nodeName string, node corev1.Node, present bool) error {
if nodeName == "" {
return errors.New("cloudprovider: reconcile was given an empty node name")
}
if !present {
r.log("cloud controller manager: node gone from the cache", map[string]any{
"cluster": r.cluster,
"node": nodeName,
})
return nil
}
inst, known, err := r.instances.Get(ctx, nodeName)
if err != nil {
return fmt.Errorf(
"cloudprovider: the instance source for %s could not be read: %w. An unreadable "+
"source is NOT an absent instance, and acting on it would delete the Node of a "+
"guest that is still running its work, so nothing is written and the Node is "+
"requeued", nodeName, err)
}
if known && inst.Cluster == "" {
return fmt.Errorf(
"cloudprovider: the instance record for %s names no cluster. A record with no "+
"cluster is MALFORMED, not absent: it yields no provider ID, and stamping the "+
"Node anyway would leave whatever identity it carries -- one from a previous "+
"life -- in place while its cluster label was overwritten. Nothing is written "+
"and the Node is requeued", nodeName)
}
if known && inst.NodeName == "" {
return fmt.Errorf(
"cloudprovider: the instance record for %s names no node. A record with no node is "+
"MALFORMED, not absent: it yields no provider ID, and nothing may be written "+
"against it, so the Node is requeued", nodeName)
}
patch, changed := Desired(node, inst, known)
if !changed {
return nil
}
if patch.Delete {
if err := r.nodes.DeleteNode(ctx, nodeName); err != nil {
return fmt.Errorf("cloudprovider: deleting the Node %s because %s: %w",
nodeName, patch.Reason, err)
}
r.log("cloud controller manager: node deleted", map[string]any{
"phase": "Reconciled",
"cluster": r.cluster,
"node": nodeName,
"reason": patch.Reason,
})
return nil
}
if patch.ProviderID != "" && node.Spec.ProviderID != patch.ProviderID {
if err := r.nodes.PatchProviderID(ctx, nodeName, patch.ProviderID); err != nil {
return fmt.Errorf("cloudprovider: stamping %s on the Node %s: %w",
patch.ProviderID, nodeName, err)
}
}
if len(patch.SetLabels)+len(patch.RemoveLabels) > 0 {
if err := r.nodes.SetLabels(ctx, nodeName, patch.SetLabels, patch.RemoveLabels); err != nil {
return fmt.Errorf("cloudprovider: labelling the Node %s: %w", nodeName, err)
}
}
r.log("cloud controller manager: node stamped", map[string]any{
"phase": "Reconciled",
"cluster": r.cluster,
"node": nodeName,
"providerID": patch.ProviderID,
"reason": patch.Reason,
})
return nil
}