github.com/IBM-Blockchain/fabric-operator@v1.0.4/controllers/ibpconsole/ibpconsole_controller.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 ibpconsole 20 21 import ( 22 "context" 23 "fmt" 24 "os" 25 "reflect" 26 "time" 27 28 current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1" 29 commoncontroller "github.com/IBM-Blockchain/fabric-operator/controllers/common" 30 config "github.com/IBM-Blockchain/fabric-operator/operatorconfig" 31 "github.com/IBM-Blockchain/fabric-operator/pkg/global" 32 "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient" 33 k8sclient "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient" 34 "github.com/IBM-Blockchain/fabric-operator/pkg/offering" 35 baseconsole "github.com/IBM-Blockchain/fabric-operator/pkg/offering/base/console" 36 "github.com/IBM-Blockchain/fabric-operator/pkg/offering/common" 37 k8sconsole "github.com/IBM-Blockchain/fabric-operator/pkg/offering/k8s/console" 38 openshiftconsole "github.com/IBM-Blockchain/fabric-operator/pkg/offering/openshift/console" 39 "github.com/IBM-Blockchain/fabric-operator/pkg/operatorerrors" 40 "github.com/IBM-Blockchain/fabric-operator/pkg/util" 41 "github.com/pkg/errors" 42 "gopkg.in/yaml.v2" 43 44 appsv1 "k8s.io/api/apps/v1" 45 corev1 "k8s.io/api/core/v1" 46 k8serrors "k8s.io/apimachinery/pkg/api/errors" 47 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 48 "k8s.io/apimachinery/pkg/labels" 49 "k8s.io/apimachinery/pkg/runtime" 50 "k8s.io/apimachinery/pkg/types" 51 ctrl "sigs.k8s.io/controller-runtime" 52 "sigs.k8s.io/controller-runtime/pkg/client" 53 "sigs.k8s.io/controller-runtime/pkg/controller" 54 "sigs.k8s.io/controller-runtime/pkg/event" 55 "sigs.k8s.io/controller-runtime/pkg/handler" 56 logf "sigs.k8s.io/controller-runtime/pkg/log" 57 "sigs.k8s.io/controller-runtime/pkg/manager" 58 "sigs.k8s.io/controller-runtime/pkg/predicate" 59 "sigs.k8s.io/controller-runtime/pkg/reconcile" 60 "sigs.k8s.io/controller-runtime/pkg/source" 61 ) 62 63 var log = logf.Log.WithName("controller_ibpconsole") 64 65 // Add creates a new IBPPeer Controller and adds it to the Manager. The Manager will set fields on the Controller 66 // and Start it when the Manager is Started. 67 func Add(mgr manager.Manager, config *config.Config) error { 68 r, err := newReconciler(mgr, config) 69 if err != nil { 70 return err 71 } 72 return add(mgr, r) 73 } 74 75 // newReconciler returns a new reconcile.Reconciler 76 func newReconciler(mgr manager.Manager, cfg *config.Config) (*ReconcileIBPConsole, error) { 77 client := k8sclient.New(mgr.GetClient(), &global.ConfigSetter{Config: cfg.Operator.Globals}) 78 scheme := mgr.GetScheme() 79 80 ibpconsole := &ReconcileIBPConsole{ 81 client: client, 82 scheme: scheme, 83 Config: cfg, 84 } 85 86 switch cfg.Offering { 87 case offering.K8S: 88 ibpconsole.Offering = k8sconsole.New(client, scheme, cfg) 89 case offering.OPENSHIFT: 90 ibpconsole.Offering = openshiftconsole.New(client, scheme, cfg) 91 } 92 93 return ibpconsole, nil 94 } 95 96 // add adds a new Controller to mgr with r as the reconcile.Reconciler 97 func add(mgr manager.Manager, r *ReconcileIBPConsole) error { 98 // Create a new controller 99 predicateFuncs := predicate.Funcs{ 100 CreateFunc: r.CreateFunc, 101 UpdateFunc: r.UpdateFunc, 102 } 103 104 c, err := controller.New("ibpconsole-controller", mgr, controller.Options{Reconciler: r}) 105 if err != nil { 106 return err 107 } 108 109 // Watch for changes to primary resource IBPConsole 110 err = c.Watch(&source.Kind{Type: ¤t.IBPConsole{}}, &handler.EnqueueRequestForObject{}, predicateFuncs) 111 if err != nil { 112 return err 113 } 114 115 // Watch for changes to secondary resource Pods and requeue the owner IBPPeer 116 err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{ 117 IsController: true, 118 OwnerType: ¤t.IBPConsole{}, 119 }) 120 if err != nil { 121 return err 122 } 123 124 return nil 125 } 126 127 var _ reconcile.Reconciler = &ReconcileIBPConsole{} 128 129 //go:generate counterfeiter -o mocks/consolereconcile.go -fake-name ConsoleReconcile . consoleReconcile 130 131 type consoleReconcile interface { 132 Reconcile(*current.IBPConsole, baseconsole.Update) (common.Result, error) 133 } 134 135 // ReconcileIBPConsole reconciles a IBPConsole object 136 type ReconcileIBPConsole struct { 137 // This client, initialized using mgr.Client() above, is a split client 138 // that reads objects from the cache and writes to the apiserver 139 client k8sclient.Client 140 scheme *runtime.Scheme 141 142 Offering consoleReconcile 143 Config *config.Config 144 145 update Update 146 } 147 148 // Reconcile reads that state of the cluster for a IBPConsole object and makes changes based on the state read 149 // and what is in the IBPConsole.Spec 150 // Note: 151 // The Controller will requeue the Request to be processed again if the returned error is non-nil or 152 // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 153 func (r *ReconcileIBPConsole) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { 154 var err error 155 156 reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 157 reqLogger.Info(fmt.Sprintf("Reconciling IBPConsole with update values of [ %+v ]", r.update.GetUpdateStackWithTrues())) 158 159 // Fetch the IBPConsole instance 160 instance := ¤t.IBPConsole{} 161 err = r.client.Get(context.TODO(), request.NamespacedName, instance) 162 if err != nil { 163 if k8serrors.IsNotFound(err) { 164 // Request object not found, could have been deleted after reconcile request. 165 // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 166 // Return and don't requeue 167 return reconcile.Result{}, nil 168 } 169 // Error reading the object - requeue the request. 170 return reconcile.Result{}, err 171 } 172 173 result, err := r.Offering.Reconcile(instance, &r.update) 174 setStatusErr := r.SetStatus(instance, err) 175 if setStatusErr != nil { 176 return reconcile.Result{}, operatorerrors.IsBreakingError(setStatusErr, "failed to update status", log) 177 } 178 179 if err != nil { 180 return reconcile.Result{}, operatorerrors.IsBreakingError(errors.Wrapf(err, "Console instance '%s' encountered error", instance.GetName()), "stopping reconcile loop", log) 181 } 182 183 reqLogger.Info(fmt.Sprintf("Finished reconciling IBPConsole '%s' with update values of [ %+v ]", instance.GetName(), r.update.GetUpdateStackWithTrues())) 184 return result.Result, nil 185 } 186 187 func (r *ReconcileIBPConsole) SetStatus(instance *current.IBPConsole, reconcileErr error) error { 188 err := r.SaveSpecState(instance) 189 if err != nil { 190 return errors.Wrap(err, "failed to save spec state") 191 } 192 193 err = r.client.Get(context.TODO(), types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, instance) 194 if err != nil { 195 return err 196 } 197 198 status := instance.Status.CRStatus 199 200 if reconcileErr != nil { 201 status.Type = current.Error 202 status.Status = current.True 203 status.Reason = "errorOccurredDuringReconcile" 204 status.Message = reconcileErr.Error() 205 status.LastHeartbeatTime = time.Now().String() 206 status.ErrorCode = operatorerrors.GetErrorCode(reconcileErr) 207 208 instance.Status = current.IBPConsoleStatus{ 209 CRStatus: status, 210 } 211 212 log.Info(fmt.Sprintf("Updating status of IBPConsole custom resource to %s phase", instance.Status.Type)) 213 err := r.client.PatchStatus(context.TODO(), instance, nil, k8sclient.PatchOption{ 214 Resilient: &k8sclient.ResilientPatch{ 215 Retry: 2, 216 Into: ¤t.IBPConsole{}, 217 Strategy: client.MergeFrom, 218 }, 219 }) 220 if err != nil { 221 return err 222 } 223 224 return nil 225 } 226 227 status.Versions.Reconciled = instance.Spec.Version 228 229 running, err := r.GetPodStatus(instance) 230 if err != nil { 231 return err 232 } 233 234 if running { 235 if instance.Status.Type == current.Deployed { 236 return nil 237 } 238 status.Type = current.Deployed 239 status.Status = current.True 240 status.Reason = "allPodsRunning" 241 } else { 242 if instance.Status.Type == current.Deploying { 243 return nil 244 } 245 status.Type = current.Deploying 246 status.Status = current.True 247 status.Reason = "waitingForPods" 248 } 249 250 instance.Status = current.IBPConsoleStatus{ 251 CRStatus: status, 252 } 253 instance.Status.LastHeartbeatTime = time.Now().String() 254 log.Info(fmt.Sprintf("Updating status of IBPConsole custom resource to %s phase", instance.Status.Type)) 255 err = r.client.PatchStatus(context.TODO(), instance, nil, k8sclient.PatchOption{ 256 Resilient: &k8sclient.ResilientPatch{ 257 Retry: 2, 258 Into: ¤t.IBPConsole{}, 259 Strategy: client.MergeFrom, 260 }, 261 }) 262 if err != nil { 263 return err 264 } 265 266 return nil 267 } 268 269 func (r *ReconcileIBPConsole) GetPodStatus(instance *current.IBPConsole) (bool, error) { 270 labelSelector, err := labels.Parse(fmt.Sprintf("app=%s", instance.Name)) 271 if err != nil { 272 return false, errors.Wrap(err, "failed to parse label selector for app name") 273 } 274 275 listOptions := &client.ListOptions{ 276 LabelSelector: labelSelector, 277 Namespace: instance.Namespace, 278 } 279 280 podList := &corev1.PodList{} 281 err = r.client.List(context.TODO(), podList, listOptions) 282 if err != nil { 283 return false, err 284 } 285 286 for _, pod := range podList.Items { 287 if pod.Status.Phase != corev1.PodRunning { 288 return false, nil 289 } 290 } 291 292 return true, nil 293 } 294 295 func (r *ReconcileIBPConsole) getIgnoreDiffs() []string { 296 return []string{ 297 `Template\.Spec\.Containers\.slice\[\d\]\.Resources\.Requests\.map\[memory\].s`, 298 } 299 } 300 301 func (r *ReconcileIBPConsole) getLabels(instance v1.Object) map[string]string { 302 label := os.Getenv("OPERATOR_LABEL_PREFIX") 303 if label == "" { 304 label = "fabric" 305 } 306 307 return map[string]string{ 308 "app": instance.GetName(), 309 "creator": label, 310 "release": "operator", 311 "helm.sh/chart": "ibm-" + label, 312 "app.kubernetes.io/name": label, 313 "app.kubernetes.io/instance": label + "console", 314 "app.kubernetes.io/managed-by": label + "-operator", 315 } 316 } 317 318 func (r *ReconcileIBPConsole) getSelectorLabels(instance v1.Object) map[string]string { 319 return map[string]string{ 320 "app": instance.GetName(), 321 } 322 } 323 324 func (r *ReconcileIBPConsole) CreateFunc(e event.CreateEvent) bool { 325 r.update = Update{} 326 327 console := e.Object.(*current.IBPConsole) 328 if console.Status.HasType() { 329 cm, err := r.GetSpecState(console) 330 if err != nil { 331 log.Info(fmt.Sprintf("Failed getting saved console spec '%s', can't perform update checks, triggering reconcile: %s", console.GetName(), err.Error())) 332 return true 333 } 334 335 specBytes := cm.BinaryData["spec"] 336 savedConsole := ¤t.IBPConsole{} 337 338 err = yaml.Unmarshal(specBytes, &savedConsole.Spec) 339 if err != nil { 340 log.Info(fmt.Sprintf("Unmarshal failed for saved console spec '%s', can't perform update checks, triggering reconcile: %s", console.GetName(), err.Error())) 341 return true 342 } 343 344 if !reflect.DeepEqual(console.Spec, savedConsole.Spec) { 345 log.Info(fmt.Sprintf("IBPConsole '%s' spec was updated while operator was down, triggering reconcile", console.GetName())) 346 r.update.specUpdated = true 347 348 if r.DeployerCMUpdated(console.Spec, savedConsole.Spec) { 349 r.update.deployerCMUpdated = true 350 } 351 if r.ConsoleCMUpdated(console.Spec, savedConsole.Spec) { 352 r.update.consoleCMUpdated = true 353 } 354 if r.EnvCMUpdated(console.Spec, savedConsole.Spec) { 355 r.update.envCMUpdated = true 356 } 357 358 return true 359 } 360 361 // Don't trigger reconcile if spec was not updated during operator restart 362 return false 363 } 364 365 // If creating resource for the first time, check that a unique name is provided 366 err := commoncontroller.ValidateCRName(r.client, console.Name, console.Namespace, commoncontroller.IBPCONSOLE) 367 if err != nil { 368 log.Error(err, "failed to validate console name") 369 operror := operatorerrors.Wrap(err, operatorerrors.InvalidCustomResourceCreateRequest, "failed to validate ibpconsole name") 370 err = r.SetStatus(console, operror) 371 if err != nil { 372 log.Error(err, "failed to set status to error", "console.name", console.Name, "error", "InvalidCustomResourceCreateRequest") 373 } 374 375 return false 376 } 377 378 return true 379 } 380 381 func (r *ReconcileIBPConsole) UpdateFunc(e event.UpdateEvent) bool { 382 r.update = Update{} 383 384 oldConsole := e.ObjectOld.(*current.IBPConsole) 385 newConsole := e.ObjectNew.(*current.IBPConsole) 386 387 if util.CheckIfZoneOrRegionUpdated(oldConsole.Spec.Zone, newConsole.Spec.Zone) { 388 log.Error(errors.New("Zone update is not allowed"), "invalid spec update") 389 return false 390 } 391 392 if util.CheckIfZoneOrRegionUpdated(oldConsole.Spec.Region, newConsole.Spec.Region) { 393 log.Error(errors.New("Region update is not allowed"), "invalid spec update") 394 return false 395 } 396 397 if reflect.DeepEqual(oldConsole.Spec, newConsole.Spec) { 398 return false 399 } 400 401 log.Info(fmt.Sprintf("Spec update detected on IBPConsole custom resource: %s", oldConsole.Name)) 402 r.update.specUpdated = true 403 404 if newConsole.Spec.Action.Restart == true { 405 r.update.restartNeeded = true 406 } 407 408 return true 409 } 410 411 func (r *ReconcileIBPConsole) SaveSpecState(instance *current.IBPConsole) error { 412 data, err := yaml.Marshal(instance.Spec) 413 if err != nil { 414 return err 415 } 416 417 cm := &corev1.ConfigMap{ 418 ObjectMeta: v1.ObjectMeta{ 419 Name: fmt.Sprintf("%s-spec", instance.GetName()), 420 Namespace: instance.GetNamespace(), 421 Labels: instance.GetLabels(), 422 }, 423 BinaryData: map[string][]byte{ 424 "spec": data, 425 }, 426 } 427 428 err = r.client.CreateOrUpdate(context.TODO(), cm, controllerclient.CreateOrUpdateOption{Owner: instance, Scheme: r.scheme}) 429 if err != nil { 430 return err 431 } 432 433 return nil 434 } 435 436 func (r *ReconcileIBPConsole) GetSpecState(instance *current.IBPConsole) (*corev1.ConfigMap, error) { 437 cm := &corev1.ConfigMap{} 438 nn := types.NamespacedName{ 439 Name: fmt.Sprintf("%s-spec", instance.GetName()), 440 Namespace: instance.GetNamespace(), 441 } 442 443 err := r.client.Get(context.TODO(), nn, cm) 444 if err != nil { 445 return nil, err 446 } 447 448 return cm, nil 449 } 450 451 func (r *ReconcileIBPConsole) DeployerCMUpdated(old, new current.IBPConsoleSpec) bool { 452 if !reflect.DeepEqual(old.ImagePullSecrets, new.ImagePullSecrets) { 453 return true 454 } 455 if !reflect.DeepEqual(old.Deployer, new.Deployer) { 456 return true 457 } 458 if old.NetworkInfo.Domain != new.NetworkInfo.Domain { 459 return true 460 } 461 if old.RegistryURL != new.RegistryURL { 462 return true 463 } 464 if !reflect.DeepEqual(old.Arch, new.Arch) { 465 return true 466 } 467 if !reflect.DeepEqual(old.Versions, new.Versions) { 468 return true 469 } 470 // Uncomment if MustGather changes are ported into release 2.5.2 471 // if old.Images.MustgatherImage != new.Images.MustgatherImage { 472 // return true 473 // } 474 // if old.Images.MustgatherTag != new.Images.MustgatherTag { 475 // return true 476 // } 477 if !reflect.DeepEqual(old.Storage, new.Storage) { 478 return true 479 } 480 if !reflect.DeepEqual(old.CRN, new.CRN) { 481 return true 482 } 483 484 oldOverrides, err := old.GetOverridesDeployer() 485 if err != nil { 486 return false 487 } 488 newOverrides, err := new.GetOverridesDeployer() 489 if err != nil { 490 return false 491 } 492 if !reflect.DeepEqual(oldOverrides, newOverrides) { 493 return true 494 } 495 496 return false 497 } 498 499 func (r *ReconcileIBPConsole) ConsoleCMUpdated(old, new current.IBPConsoleSpec) bool { 500 if !reflect.DeepEqual(old.IBMID, new.IBMID) { 501 return true 502 } 503 if old.IAMApiKey != new.IAMApiKey { 504 return true 505 } 506 if old.SegmentWriteKey != new.SegmentWriteKey { 507 return true 508 } 509 if old.Email != new.Email { 510 return true 511 } 512 if old.AuthScheme != new.AuthScheme { 513 return true 514 } 515 if old.ConfigtxlatorURL != new.ConfigtxlatorURL { 516 return true 517 } 518 if old.DeployerURL != new.DeployerURL { 519 return true 520 } 521 if old.DeployerTimeout != new.DeployerTimeout { 522 return true 523 } 524 if old.Components != new.Components { 525 return true 526 } 527 if old.Sessions != new.Sessions { 528 return true 529 } 530 if old.System != new.System { 531 return true 532 } 533 if old.SystemChannel != new.SystemChannel { 534 return true 535 } 536 if !reflect.DeepEqual(old.Proxying, new.Proxying) { 537 return true 538 } 539 if !reflect.DeepEqual(old.FeatureFlags, new.FeatureFlags) { 540 return true 541 } 542 if !reflect.DeepEqual(old.ClusterData, new.ClusterData) { 543 return true 544 } 545 if !reflect.DeepEqual(old.CRN, new.CRN) { 546 return true 547 } 548 549 oldOverrides, err := old.GetOverridesConsole() 550 if err != nil { 551 return false 552 } 553 newOverrides, err := new.GetOverridesConsole() 554 if err != nil { 555 return false 556 } 557 if !reflect.DeepEqual(oldOverrides, newOverrides) { 558 return true 559 } 560 561 return false 562 } 563 564 func (r *ReconcileIBPConsole) EnvCMUpdated(old, new current.IBPConsoleSpec) bool { 565 if old.ConnectionString != new.ConnectionString { 566 return true 567 } 568 if old.System != new.System { 569 return true 570 } 571 if old.TLSSecretName != new.TLSSecretName { 572 return true 573 } 574 575 return false 576 } 577 578 func (r *ReconcileIBPConsole) SetupWithManager(mgr ctrl.Manager) error { 579 return ctrl.NewControllerManagedBy(mgr). 580 For(¤t.IBPConsole{}). 581 Complete(r) 582 } 583 584 type Update struct { 585 specUpdated bool 586 restartNeeded bool 587 deployerCMUpdated bool 588 consoleCMUpdated bool 589 envCMUpdated bool 590 } 591 592 func (u *Update) SpecUpdated() bool { 593 return u.specUpdated 594 } 595 596 func (u *Update) RestartNeeded() bool { 597 return u.restartNeeded 598 } 599 600 func (u *Update) DeployerCMUpdated() bool { 601 return u.deployerCMUpdated 602 } 603 604 func (u *Update) ConsoleCMUpdated() bool { 605 return u.consoleCMUpdated 606 } 607 608 func (u *Update) EnvCMUpdated() bool { 609 return u.envCMUpdated 610 } 611 612 func (u *Update) GetUpdateStackWithTrues() string { 613 stack := "" 614 615 if u.specUpdated { 616 stack += "specUpdated " 617 } 618 if u.restartNeeded { 619 stack += "restartNeeded " 620 } 621 if u.deployerCMUpdated { 622 stack += "deployerCMUpdated " 623 } 624 if u.consoleCMUpdated { 625 stack += "consoleCMUpdated " 626 } 627 if u.envCMUpdated { 628 stack += "envCMUpdated " 629 } 630 631 if len(stack) == 0 { 632 stack = "emptystack " 633 } 634 635 return stack 636 }