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  }