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: &current.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:    &current.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 := &current.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:     &current.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:     &current.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 := &current.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(&current.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  }