github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/manager/resources/deployment/manager.go (about) 1 /* 2 * Copyright contributors to the Hyperledger Fabric Operator project 3 * 4 * SPDX-License-Identifier: Apache-2.0 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package deployment 20 21 import ( 22 "context" 23 "fmt" 24 "os" 25 "regexp" 26 27 k8sclient "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient" 28 "github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources" 29 "github.com/IBM-Blockchain/fabric-operator/pkg/operatorerrors" 30 "github.com/IBM-Blockchain/fabric-operator/pkg/util" 31 "github.com/go-test/deep" 32 "github.com/pkg/errors" 33 appsv1 "k8s.io/api/apps/v1" 34 k8serrors "k8s.io/apimachinery/pkg/api/errors" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 37 "k8s.io/apimachinery/pkg/runtime" 38 "k8s.io/apimachinery/pkg/types" 39 "sigs.k8s.io/controller-runtime/pkg/client" 40 logf "sigs.k8s.io/controller-runtime/pkg/log" 41 ) 42 43 var log = logf.Log.WithName("deployment_manager") 44 45 type Manager struct { 46 Client k8sclient.Client 47 Scheme *runtime.Scheme 48 DeploymentFile string 49 IgnoreDifferences []string 50 Name string 51 52 LabelsFunc func(v1.Object) map[string]string 53 OverrideFunc func(v1.Object, *appsv1.Deployment, resources.Action) error 54 } 55 56 func (m *Manager) GetName(instance v1.Object) string { 57 return GetName(instance.GetName(), m.Name) 58 } 59 60 func (m *Manager) Reconcile(instance v1.Object, update bool) error { 61 name := m.GetName(instance) 62 63 deployment := &appsv1.Deployment{} 64 err := m.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.GetNamespace()}, deployment) 65 if err != nil { 66 if k8serrors.IsNotFound(err) { 67 log.Info(fmt.Sprintf("Creating deployment '%s'", name)) 68 deployment, err := m.GetDeploymentBasedOnCRFromFile(instance) 69 if err != nil { 70 return err 71 } 72 73 err = m.Client.Create(context.TODO(), deployment, k8sclient.CreateOption{ 74 Owner: instance, 75 Scheme: m.Scheme, 76 }) 77 if err != nil { 78 return err 79 } 80 return nil 81 } 82 return err 83 } 84 85 if update { 86 log.Info(fmt.Sprintf("Updating deployment '%s'", name)) 87 err = m.OverrideFunc(instance, deployment, resources.Update) 88 if err != nil { 89 return operatorerrors.New(operatorerrors.InvalidDeploymentUpdateRequest, err.Error()) 90 } 91 92 err = m.Client.Patch(context.TODO(), deployment, nil, k8sclient.PatchOption{ 93 Resilient: &k8sclient.ResilientPatch{ 94 Retry: 3, 95 Into: &appsv1.Deployment{}, 96 Strategy: client.MergeFrom, 97 }, 98 }) 99 if err != nil { 100 return err 101 } 102 103 // Wait for deployment to get updated before returning 104 105 // TODO: Currently commented this out because with the rolling updates (i.e. for console), 106 // it takes longer to wait for the new pod to come up and be running and for the 107 // old pod to then terminate. Need to figure out how to resolve this. 108 // err := wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) { 109 // upToDate := m.DeploymentIsUpToDate(instance) 110 // if upToDate { 111 // return true, nil 112 // } 113 // return false, nil 114 // }) 115 // if err != nil { 116 // return errors.Wrap(err, "failed to determine if deployment was updated") 117 // } 118 } 119 120 return nil 121 } 122 123 func (m *Manager) GetDeploymentBasedOnCRFromFile(instance v1.Object) (*appsv1.Deployment, error) { 124 deployment, err := util.GetDeploymentFromFile(m.DeploymentFile) 125 if err != nil { 126 log.Error(err, fmt.Sprintf("Error reading deployment configuration file: %s", m.DeploymentFile)) 127 return nil, err 128 } 129 130 return m.BasedOnCR(instance, deployment) 131 } 132 133 func (m *Manager) CheckForSecretChange(instance v1.Object, secretName string, restartFunc func(string, *appsv1.Deployment) bool) error { 134 name := m.GetName(instance) 135 136 deployment := &appsv1.Deployment{} 137 err := m.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.GetNamespace()}, deployment) 138 if err != nil { 139 if k8serrors.IsNotFound(err) { 140 return nil 141 } 142 return err 143 } 144 145 rv, err := util.GetResourceVerFromSecret(m.Client, secretName, instance.GetNamespace()) 146 if err == nil && rv != "" { 147 // Only if secret change is detected do we update deployment env var with new resource version 148 changed := restartFunc(rv, deployment) 149 if changed { 150 log.Info(fmt.Sprintf("Secret '%s' update detected, triggering deployment restart for peer '%s'", secretName, instance.GetName())) 151 err = m.Client.Update(context.TODO(), deployment) 152 if err != nil { 153 return errors.Wrap(err, "failed to update deployment with secret resource version") 154 } 155 } 156 } 157 158 return nil 159 160 } 161 162 func (m *Manager) BasedOnCR(instance v1.Object, deployment *appsv1.Deployment) (*appsv1.Deployment, error) { 163 if m.OverrideFunc != nil { 164 err := m.OverrideFunc(instance, deployment, resources.Create) 165 if err != nil { 166 return nil, operatorerrors.New(operatorerrors.InvalidDeploymentCreateRequest, err.Error()) 167 } 168 } 169 170 deployment.Name = m.GetName(instance) 171 deployment.Namespace = instance.GetNamespace() 172 requiredLabels := m.LabelsFunc(instance) 173 labels := deployment.Labels 174 if len(labels) == 0 { 175 labels = make(map[string]string) 176 } 177 for requiredKey, requiredElement := range requiredLabels { 178 labels[requiredKey] = requiredElement 179 } 180 deployment.Labels = labels 181 deployment.Spec.Template.Labels = labels 182 deployment.Spec.Selector = &metav1.LabelSelector{ 183 MatchLabels: m.getSelectorLabels(instance), 184 } 185 186 return deployment, nil 187 } 188 189 func (m *Manager) CheckState(instance v1.Object) error { 190 if instance == nil { 191 return nil // Instance has not been reconciled yet 192 } 193 194 name := m.GetName(instance) 195 196 // Get the latest version of the instance 197 deployment := &appsv1.Deployment{} 198 err := m.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.GetNamespace()}, deployment) 199 if err != nil { 200 return nil 201 } 202 203 copy := deployment.DeepCopy() 204 expectedDeployment, err := m.BasedOnCR(instance, copy) 205 if err != nil { 206 return err 207 } 208 209 deep.MaxDepth = 20 210 deep.MaxDiff = 30 211 deep.CompareUnexportedFields = true 212 deep.LogErrors = true 213 214 if os.Getenv("OPERATOR_DEBUG_DISABLEDEPLOYMENTCHECKS") == "true" { 215 return nil 216 } 217 218 diff := deep.Equal(deployment.Spec, expectedDeployment.Spec) 219 if diff != nil { 220 err := m.ignoreDifferences(diff) 221 if err != nil { 222 return errors.Wrap(err, fmt.Sprintf("deployment (%s) has been edited manually, and does not match what is expected based on the CR", deployment.GetName())) 223 } 224 } 225 226 return nil 227 } 228 229 func (m *Manager) RestoreState(instance v1.Object) error { 230 if instance == nil { 231 return nil // Instance has not been reconciled yet 232 } 233 234 name := m.GetName(instance) 235 deployment := &appsv1.Deployment{} 236 err := m.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.GetNamespace()}, deployment) 237 if err != nil { 238 return nil 239 } 240 241 deployment, err = m.BasedOnCR(instance, deployment) 242 if err != nil { 243 return err 244 } 245 246 err = m.Client.Patch(context.TODO(), deployment, nil, k8sclient.PatchOption{ 247 Resilient: &k8sclient.ResilientPatch{ 248 Retry: 2, 249 Into: &appsv1.Deployment{}, 250 Strategy: client.MergeFrom, 251 }, 252 }) 253 if err != nil { 254 return err 255 } 256 257 return nil 258 } 259 260 func (m *Manager) Get(instance v1.Object) (client.Object, error) { 261 if instance == nil { 262 return nil, nil // Instance has not been reconciled yet 263 } 264 265 name := m.GetName(instance) 266 deployment := &appsv1.Deployment{} 267 err := m.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.GetNamespace()}, deployment) 268 if err != nil { 269 return nil, err 270 } 271 272 return deployment, nil 273 } 274 275 func (m *Manager) Exists(instance v1.Object) bool { 276 dep, err := m.Get(instance) 277 if err != nil || dep == nil { 278 return false 279 } 280 281 return true 282 } 283 284 func (m *Manager) Delete(instance v1.Object) error { 285 dep, err := m.Get(instance) 286 if err != nil { 287 if !k8serrors.IsNotFound(err) { 288 return err 289 } 290 } 291 292 if dep == nil { 293 return nil 294 } 295 296 err = m.Client.Delete(context.TODO(), dep) 297 if err != nil { 298 if !k8serrors.IsNotFound(err) { 299 return err 300 } 301 } 302 303 return nil 304 } 305 306 func (m *Manager) getSelectorLabels(instance v1.Object) map[string]string { 307 return map[string]string{ 308 "app": instance.GetName(), 309 } 310 } 311 312 func (m *Manager) ignoreDifferences(diff []string) error { 313 diffs := []string{} 314 for _, d := range diff { 315 found := false 316 for _, i := range m.differenceToIgnore() { 317 regex := regexp.MustCompile(i) 318 found = regex.MatchString(d) 319 if found { 320 break 321 } 322 } 323 if !found { 324 diffs = append(diffs, d) 325 return fmt.Errorf("unexpected mismatch: %s", d) 326 } 327 } 328 return nil 329 } 330 331 func (m *Manager) differenceToIgnore() []string { 332 d := []string{ 333 "TypeMeta", "ObjectMeta", 334 "VolumeSource.Secret.DefaultMode", 335 "VolumeSource.ConfigMap.DefaultMode", 336 "TerminationMessagePath", 337 "TerminationMessagePolicy", 338 "SecurityContext.ProcMount", 339 "Template.Spec.TerminationGracePeriodSeconds", 340 "Template.Spec.DNSPolicy", 341 "Template.Spec.DeprecatedServiceAccount", 342 "Template.Spec.SchedulerName", 343 "RevisionHistoryLimit", 344 "RestartPolicy", 345 "ProgressDeadlineSeconds", 346 "LivenessProbe.SuccessThreshold", 347 "LivenessProbe.FailureThreshold", 348 "LivenessProbe.InitialDelaySeconds", 349 "LivenessProbe.PeriodSeconds", 350 "LivenessProbe.TimeoutSeconds", 351 "ReadinessProbe.SuccessThreshold", 352 "ReadinessProbe.FailureThreshold", 353 "ReadinessProbe.InitialDelaySeconds", 354 "ReadinessProbe.PeriodSeconds", 355 "ReadinessProbe.TimeoutSeconds", 356 "StartupProbe.SuccessThreshold", 357 "StartupProbe.FailureThreshold", 358 "StartupProbe.InitialDelaySeconds", 359 "StartupProbe.PeriodSeconds", 360 "StartupProbe.TimeoutSeconds", 361 "ValueFrom.FieldRef.APIVersion", 362 "Template.Spec.Affinity", 363 "Template.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms", 364 "Strategy.RollingUpdate", 365 } 366 d = append(d, m.IgnoreDifferences...) 367 return d 368 } 369 370 func (m *Manager) DeploymentIsUpToDate(instance v1.Object) bool { 371 deployment := &appsv1.Deployment{} 372 err := m.Client.Get( 373 context.TODO(), 374 types.NamespacedName{Name: m.GetName(instance), Namespace: instance.GetNamespace()}, 375 deployment, 376 ) 377 if err != nil { 378 return false 379 } 380 381 if deployment.Status.Replicas > 0 { 382 if deployment.Status.Replicas != deployment.Status.UpdatedReplicas { 383 return false 384 } 385 } 386 387 return true 388 } 389 390 func (m *Manager) DeploymentStatus(instance v1.Object) (appsv1.DeploymentStatus, error) { 391 deployment := &appsv1.Deployment{} 392 err := m.Client.Get( 393 context.TODO(), 394 types.NamespacedName{Name: m.GetName(instance), Namespace: instance.GetNamespace()}, 395 deployment, 396 ) 397 if err != nil { 398 return appsv1.DeploymentStatus{}, err 399 } 400 401 return deployment.Status, nil 402 } 403 404 func (m *Manager) SetCustomName(name string) { 405 // NO-OP 406 } 407 408 func (m *Manager) GetScheme() *runtime.Scheme { 409 return m.Scheme 410 } 411 412 func GetName(instanceName string, suffix ...string) string { 413 if len(suffix) != 0 { 414 if suffix[0] != "" { 415 return fmt.Sprintf("%s-%s", instanceName, suffix[0]) 416 } 417 } 418 return fmt.Sprintf("%s", instanceName) 419 }