@@ -16,8 +16,17 @@ package runtime
16
16
17
17
import (
18
18
"context"
19
+ "errors"
20
+ "sync"
21
+ "time"
19
22
23
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20
24
"k8s.io/apimachinery/pkg/runtime/schema"
25
+ "k8s.io/apimachinery/pkg/util/sets"
26
+ "k8s.io/client-go/discovery"
27
+ "k8s.io/client-go/rest"
28
+
29
+ "github.com/crunchydata/postgres-operator/internal/logging"
21
30
)
22
31
23
32
// API is a combination of Group, Version, and Kind that can be used to check
@@ -73,6 +82,145 @@ func (s APISet) HasOne(api ...API) bool {
73
82
return false
74
83
}
75
84
85
+ type APIDiscoveryRunner struct {
86
+ Client interface {
87
+ ServerGroups () (* metav1.APIGroupList , error )
88
+ ServerResourcesForGroupVersion (string ) (* metav1.APIResourceList , error )
89
+ }
90
+
91
+ refresh time.Duration
92
+
93
+ want []API
94
+ have struct {
95
+ sync.RWMutex
96
+ APISet
97
+ }
98
+ }
99
+
100
+ // NewAPIDiscoveryRunner creates an [APIDiscoveryRunner] that periodically reads
101
+ // what APIs are available in the Kubernetes at config.
102
+ func NewAPIDiscoveryRunner (config * rest.Config ) (* APIDiscoveryRunner , error ) {
103
+ dc , err := discovery .NewDiscoveryClientForConfig (config )
104
+
105
+ runner := & APIDiscoveryRunner {
106
+ Client : dc ,
107
+ refresh : 10 * time .Minute ,
108
+ want : []API {
109
+ {Group : "cert-manager.io" , Kind : "Certificate" },
110
+ {Group : "gateway.networking.k8s.io" , Kind : "ReferenceGrant" },
111
+ {Group : "security.openshift.io" , Kind : "SecurityContextConstraints" },
112
+ {Group : "snapshot.storage.k8s.io" , Kind : "VolumeSnapshot" },
113
+ {Group : "trust.cert-manager.io" , Kind : "Bundle" },
114
+ },
115
+ }
116
+
117
+ return runner , err
118
+ }
119
+
120
+ // NeedLeaderElection returns false so that r runs on any [manager.Manager],
121
+ // regardless of which is elected leader in the Kubernetes namespace.
122
+ func (r * APIDiscoveryRunner ) NeedLeaderElection () bool { return false }
123
+
124
+ // Read fetches available APIs from Kubernetes.
125
+ func (r * APIDiscoveryRunner ) Read () error {
126
+
127
+ // Build an index of the APIs we want to know about.
128
+ wantAPIs := make (map [string ]map [string ]sets.Set [string ])
129
+ for _ , want := range r .want {
130
+ if wantAPIs [want .Group ] == nil {
131
+ wantAPIs [want .Group ] = make (map [string ]sets.Set [string ])
132
+ }
133
+ if wantAPIs [want.Group ][want.Version ] == nil {
134
+ wantAPIs [want.Group ][want.Version ] = sets .New [string ]()
135
+ }
136
+ if want .Kind != "" {
137
+ wantAPIs [want.Group ][want.Version ].Insert (want .Kind )
138
+ }
139
+ }
140
+
141
+ // Fetch Groups and Versions from Kubernetes.
142
+ groups , err := r .Client .ServerGroups ()
143
+ if err != nil {
144
+ return err
145
+ }
146
+
147
+ // Build an index of the Groups, GVs, GKs, and GVKs available in Kuberentes
148
+ // that we want to know about.
149
+ haveWantedAPIs := make (map [API ]struct {})
150
+ for _ , apiG := range groups .Groups {
151
+ var haveG string = apiG .Name
152
+ haveWantedAPIs [API {Group : haveG }] = struct {}{}
153
+
154
+ for _ , apiGV := range apiG .Versions {
155
+ var haveV string = apiGV .Version
156
+ haveWantedAPIs [API {Group : haveG , Version : haveV }] = struct {}{}
157
+
158
+ // Only fetch Resources when there are Kinds we want to know about.
159
+ if wantAPIs [haveG ]["" ].Len () == 0 && wantAPIs [haveG ][haveV ].Len () == 0 {
160
+ continue
161
+ }
162
+
163
+ resources , err := r .Client .ServerResourcesForGroupVersion (apiGV .GroupVersion )
164
+ if err != nil {
165
+ return err
166
+ }
167
+
168
+ for _ , apiR := range resources .APIResources {
169
+ var haveK string = apiR .Kind
170
+ haveWantedAPIs [API {Group : haveG , Kind : haveK }] = struct {}{}
171
+ haveWantedAPIs [API {Group : haveG , Kind : haveK , Version : haveV }] = struct {}{}
172
+ }
173
+ }
174
+ }
175
+
176
+ r .have .Lock ()
177
+ r .have .APISet = haveWantedAPIs
178
+ r .have .Unlock ()
179
+
180
+ return nil
181
+ }
182
+
183
+ // Start periodically reads the Kuberentes API. It blocks until ctx is cancelled.
184
+ func (r * APIDiscoveryRunner ) Start (ctx context.Context ) error {
185
+ ticker := time .NewTicker (r .refresh )
186
+ defer ticker .Stop ()
187
+
188
+ log := logging .FromContext (ctx ).WithValues ("controller" , "kubernetes" )
189
+
190
+ for {
191
+ select {
192
+ case <- ticker .C :
193
+ if err := r .Read (); err != nil {
194
+ log .Error (err , "Unable to detect Kubernetes APIs" )
195
+ }
196
+ case <- ctx .Done ():
197
+ // TODO(controller-runtime): Fixed in v0.19.0
198
+ // https://github.com/kubernetes-sigs/controller-runtime/issues/1927
199
+ if errors .Is (ctx .Err (), context .Canceled ) {
200
+ return nil
201
+ }
202
+ return ctx .Err ()
203
+ }
204
+ }
205
+ }
206
+
207
+ // Has returns true when api is available in Kuberentes.
208
+ func (r * APIDiscoveryRunner ) Has (api API ) bool { return r .HasOne (api ) }
209
+
210
+ // HasAll returns true when every api is available in Kubernetes.
211
+ func (r * APIDiscoveryRunner ) HasAll (api ... API ) bool {
212
+ r .have .RLock ()
213
+ defer r .have .RUnlock ()
214
+ return r .have .HasAll (api ... )
215
+ }
216
+
217
+ // HasOne returns true when at least one api is available in Kubernetes.
218
+ func (r * APIDiscoveryRunner ) HasOne (api ... API ) bool {
219
+ r .have .RLock ()
220
+ defer r .have .RUnlock ()
221
+ return r .have .HasOne (api ... )
222
+ }
223
+
76
224
type apiContextKey struct {}
77
225
78
226
// Kubernetes returns the APIs previously stored by [NewAPIContext].
0 commit comments