generated from kubernetes/kubernetes-template-project
-
Notifications
You must be signed in to change notification settings - Fork 89
/
Copy pathpreflight_checks.go
282 lines (231 loc) · 10.5 KB
/
preflight_checks.go
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"fmt"
"os"
"github.com/google/go-github/v52/github"
"golang.org/x/oauth2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/version"
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
"sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider"
"sigs.k8s.io/cluster-api-operator/util"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
"sigs.k8s.io/cluster-api/util/conditions"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
coreProvider = "CoreProvider"
)
var (
moreThanOneCoreProviderInstanceExistsMessage = "CoreProvider already exists in the cluster. Only one is allowed."
moreThanOneProviderInstanceExistsMessage = "There is already a %s with name %s in the cluster. Only one is allowed."
capiVersionIncompatibilityMessage = "CAPI operator is only compatible with %s providers, detected %s for provider %s."
invalidGithubTokenMessage = "Invalid github token, please check your github token value and its permissions" //nolint:gosec
waitingForCoreProviderReadyMessage = "Waiting for the core provider to be installed."
incorrectCoreProviderNameMessage = "Incorrect CoreProvider name: %s. It should be %s"
unsupportedProviderDowngradeMessage = "Downgrade is not supported for provider %s"
)
// preflightChecks performs preflight checks before installing provider.
func preflightChecks(ctx context.Context, c client.Client, provider genericprovider.GenericProvider, providerList genericprovider.GenericProviderList) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Performing preflight checks")
spec := provider.GetSpec()
if spec.Version != "" {
// Check that the provider version is supported.
if err := checkProviderVersion(ctx, spec.Version, provider); err != nil {
return err
}
}
// Ensure that the CoreProvider is called "cluster-api".
if util.IsCoreProvider(provider) {
if provider.GetName() != configclient.ClusterAPIProviderName {
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.IncorrectCoreProviderNameReason,
clusterv1.ConditionSeverityError,
"%s", fmt.Sprintf(incorrectCoreProviderNameMessage, provider.GetName(), configclient.ClusterAPIProviderName),
))
return fmt.Errorf("incorrect CoreProvider name: %s, it should be %s", provider.GetName(), configclient.ClusterAPIProviderName)
}
}
// Check that if a predefined provider is being installed, and if it's not - ensure that FetchConfig is specified.
isPredefinedProvider, err := isPredefinedProvider(ctx, provider.GetName(), util.ClusterctlProviderType(provider))
if err != nil {
return fmt.Errorf("failed to generate a list of predefined providers: %w", err)
}
if !isPredefinedProvider {
if spec.FetchConfig == nil || spec.FetchConfig.Selector == nil && spec.FetchConfig.URL == "" && spec.FetchConfig.OCI == "" {
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.FetchConfigValidationErrorReason,
clusterv1.ConditionSeverityError,
"Either Selector, OCI URL or provider URL must be provided for a not predefined provider",
))
return fmt.Errorf("either selector, OCI URL or provider URL must be provided for a not predefined provider %s", provider.GetName())
}
}
if spec.FetchConfig != nil && spec.FetchConfig.Selector != nil && spec.FetchConfig.URL != "" {
// If FetchConfiguration is not nil, exactly one of `URL` or `Selector` must be specified.
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.FetchConfigValidationErrorReason,
clusterv1.ConditionSeverityError,
"Only one of Selector and URL must be provided, not both",
))
return fmt.Errorf("only one of Selector and URL must be provided for provider %s", provider.GetName())
}
// Validate that provided GitHub token works and has repository access.
if spec.ConfigSecret != nil {
secret := &corev1.Secret{}
key := types.NamespacedName{Namespace: provider.GetSpec().ConfigSecret.Namespace, Name: provider.GetSpec().ConfigSecret.Name}
if err := c.Get(ctx, key, secret); err != nil {
return fmt.Errorf("failed to get providers secret: %w", err)
}
if token, ok := secret.Data[configclient.GitHubTokenVariable]; ok {
client := github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: string(token)},
)))
if _, _, err := client.Organizations.List(ctx, "kubernetes-sigs", nil); err != nil {
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.InvalidGithubTokenReason,
clusterv1.ConditionSeverityError,
"%s", invalidGithubTokenMessage,
))
return fmt.Errorf("failed to validate provided github token: %w", err)
}
}
}
if err := c.List(ctx, providerList); err != nil {
return fmt.Errorf("failed to list providers: %w", err)
}
// Check that no more than one instance of the provider is installed.
for _, p := range providerList.GetItems() {
// Skip if provider in the list is the same as provider it's compared with.
if p.GetNamespace() == provider.GetNamespace() && p.GetName() == provider.GetName() {
continue
}
preflightFalseCondition := conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.MoreThanOneProviderInstanceExistsReason,
clusterv1.ConditionSeverityError,
"",
)
// CoreProvider is a singleton resource, more than one instances should not exist
if util.IsCoreProvider(p) {
log.Info(moreThanOneCoreProviderInstanceExistsMessage)
preflightFalseCondition.Message = moreThanOneCoreProviderInstanceExistsMessage
conditions.Set(provider, preflightFalseCondition)
return fmt.Errorf("only one instance of CoreProvider is allowed")
}
// For any other provider we should check that instances with similar name exist in any namespace
if p.GetObjectKind().GroupVersionKind().Kind != coreProvider && p.GetName() == provider.GetName() {
preflightFalseCondition.Message = fmt.Sprintf(moreThanOneProviderInstanceExistsMessage, p.GetName(), p.GetNamespace())
log.Info(preflightFalseCondition.Message)
conditions.Set(provider, preflightFalseCondition)
return fmt.Errorf("only one %s provider is allowed in the cluster", p.GetName())
}
}
// Wait for core provider to be ready before we install other providers.
if !util.IsCoreProvider(provider) {
ready, err := coreProviderIsReady(ctx, c)
if err != nil {
return fmt.Errorf("failed to get coreProvider ready condition: %w", err)
}
if !ready {
log.Info(waitingForCoreProviderReadyMessage)
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.WaitingForCoreProviderReadyReason,
clusterv1.ConditionSeverityInfo,
"%s", waitingForCoreProviderReadyMessage,
))
return nil
}
}
conditions.Set(provider, conditions.TrueCondition(operatorv1.PreflightCheckCondition))
log.Info("Preflight checks passed")
return nil
}
// checkProviderVersion verifies that target and installed provider versions are correct.
func checkProviderVersion(ctx context.Context, providerVersion string, provider genericprovider.GenericProvider) error {
log := ctrl.LoggerFrom(ctx)
// Check that provider version contains a valid value if it's not empty.
targetVersion, err := version.ParseSemantic(providerVersion)
if err != nil {
log.Info("Version contains invalid value")
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.IncorrectVersionFormatReason,
clusterv1.ConditionSeverityError,
"%s", err.Error(),
))
return fmt.Errorf("version contains invalid value for provider %q", provider.GetName())
}
// Cluster API doesn't support downgrades by design. We need to report that for the user.
if provider.GetStatus().InstalledVersion != nil && *provider.GetStatus().InstalledVersion != "" {
installedVersion, err := version.ParseSemantic(*provider.GetStatus().InstalledVersion)
if err != nil {
return fmt.Errorf("installed version contains invalid value for provider %q", provider.GetName())
}
if targetVersion.Major() < installedVersion.Major() || targetVersion.Major() == installedVersion.Major() && targetVersion.Minor() < installedVersion.Minor() {
conditions.Set(provider, conditions.FalseCondition(
operatorv1.PreflightCheckCondition,
operatorv1.UnsupportedProviderDowngradeReason,
clusterv1.ConditionSeverityError,
"%s", unsupportedProviderDowngradeMessage,
))
return fmt.Errorf("downgrade is not supported for provider %q", provider.GetName())
}
}
return nil
}
// coreProviderIsReady returns true if the core provider is ready.
func coreProviderIsReady(ctx context.Context, c client.Client) (bool, error) {
cpl := &operatorv1.CoreProviderList{}
if err := c.List(ctx, cpl); err != nil {
return false, err
}
for _, cp := range cpl.Items {
if conditions.IsTrue(&cp, clusterv1.ReadyCondition) {
return true, nil
}
}
return false, nil
}
// isPredefinedProvider checks if a given provider is known for Cluster API.
// The list of known providers can be found here:
// https://github.com/kubernetes-sigs/cluster-api/blob/main/cmd/clusterctl/client/config/providers_client.go
func isPredefinedProvider(ctx context.Context, providerName string, providerType clusterctlv1.ProviderType) (bool, error) {
path := configPath
if _, err := os.Stat(configPath); os.IsNotExist(err) {
path = ""
} else if err != nil {
return false, err
}
// Initialize a client that contains predefined providers only.
configClient, err := configclient.New(ctx, path)
if err != nil {
return false, err
}
// Try to find given provider in the predefined ones. If there is nothing, the function returns an error.
_, err = configClient.Providers().Get(providerName, providerType)
return err == nil, nil
}