github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/offering/base/console/override/deployment.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 override
    20  
    21  import (
    22  	"encoding/json"
    23  	"net/url"
    24  
    25  	"github.com/pkg/errors"
    26  
    27  	current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1"
    28  	defaultconsole "github.com/IBM-Blockchain/fabric-operator/defaultconfig/console"
    29  	deployerimgs "github.com/IBM-Blockchain/fabric-operator/pkg/apis/deployer"
    30  	"github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources"
    31  	"github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources/container"
    32  	dep "github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources/deployment"
    33  	"github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources/serviceaccount"
    34  	"github.com/IBM-Blockchain/fabric-operator/pkg/offering/common"
    35  	"github.com/IBM-Blockchain/fabric-operator/pkg/util/image"
    36  	appsv1 "k8s.io/api/apps/v1"
    37  	corev1 "k8s.io/api/core/v1"
    38  	"k8s.io/apimachinery/pkg/api/resource"
    39  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    40  	"k8s.io/apimachinery/pkg/util/intstr"
    41  )
    42  
    43  // Container names
    44  const (
    45  	INIT          = "init"
    46  	CONSOLE       = "optools"
    47  	DEPLOYER      = "deployer"
    48  	CONFIGTXLATOR = "configtxlator"
    49  	COUCHDB       = "couchdb"
    50  )
    51  
    52  func (o *Override) Deployment(object v1.Object, deployment *appsv1.Deployment, action resources.Action) error {
    53  	instance := object.(*current.IBPConsole)
    54  	switch action {
    55  	case resources.Create:
    56  		return o.CreateDeployment(instance, deployment)
    57  	case resources.Update:
    58  		return o.UpdateDeployment(instance, deployment)
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  func (o *Override) CreateDeployment(instance *current.IBPConsole, k8sDep *appsv1.Deployment) error {
    65  	deployment := dep.New(k8sDep)
    66  
    67  	name := instance.GetName()
    68  	deployment.SetServiceAccountName(serviceaccount.GetName(name))
    69  
    70  	// Make sure containers exist
    71  	console, err := deployment.GetContainer(CONSOLE)
    72  	if err != nil {
    73  		return errors.New("console container not found in deployment spec")
    74  	}
    75  	_, err = deployment.GetContainer(INIT)
    76  	if err != nil {
    77  		return errors.New("init container not found in deployment spec")
    78  	}
    79  	_, err = deployment.GetContainer(DEPLOYER)
    80  	if err != nil {
    81  		return errors.New("deployer container not found in deployment spec")
    82  	}
    83  	_, err = deployment.GetContainer(CONFIGTXLATOR)
    84  	if err != nil {
    85  		return errors.New("configtxlator container not found in deployment spec")
    86  	}
    87  
    88  	if !instance.Spec.UsingRemoteDB() {
    89  		couchdb := o.CreateCouchdbContainer()
    90  
    91  		couchdb.AppendVolumeMountWithSubPathIfMissing("couchdb", "/opt/couchdb/data", "data")
    92  		deployment.AddContainer(couchdb)
    93  	}
    94  
    95  	err = o.CommonDeployment(instance, deployment)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	deployment.SetImagePullSecrets(instance.Spec.ImagePullSecrets)
   101  
   102  	console.AppendConfigMapFromSourceIfMissing(name)
   103  
   104  	passwordSecretName := instance.Spec.PasswordSecretName
   105  	valueFrom := &corev1.EnvVarSource{
   106  		SecretKeyRef: &corev1.SecretKeySelector{
   107  			LocalObjectReference: corev1.LocalObjectReference{
   108  				Name: passwordSecretName,
   109  			},
   110  			Key: "password",
   111  		},
   112  	}
   113  	console.AppendEnvVarValueFromIfMissing("DEFAULT_USER_PASSWORD_INITIAL", valueFrom)
   114  
   115  	tlsSecretName := instance.Spec.TLSSecretName
   116  	if tlsSecretName != "" {
   117  		console.AppendVolumeMountIfMissing("tls-certs", "/certs/tls")
   118  		deployment.AppendSecretVolumeIfMissing("tls-certs", tlsSecretName)
   119  	} else {
   120  		// TODO: generate and create the TLS Secret here itself
   121  	}
   122  
   123  	if !instance.Spec.UsingRemoteDB() {
   124  		deployment.AppendPVCVolumeIfMissing("couchdb", instance.Name+"-pvc")
   125  	}
   126  
   127  	deployment.AppendConfigMapVolumeIfMissing("deployer-template", name+"-deployer")
   128  	deployment.AppendConfigMapVolumeIfMissing("template", name+"-console")
   129  	deployment.SetAffinity(o.GetAffinity(instance))
   130  
   131  	return nil
   132  }
   133  
   134  func (o *Override) UpdateDeployment(instance *current.IBPConsole, k8sDep *appsv1.Deployment) error {
   135  	deployment := dep.New(k8sDep)
   136  	return o.CommonDeployment(instance, deployment)
   137  }
   138  
   139  func (o *Override) CommonDeployment(instance *current.IBPConsole, deployment *dep.Deployment) error {
   140  	init := deployment.MustGetContainer(INIT)
   141  	console := deployment.MustGetContainer(CONSOLE)
   142  	deployer := deployment.MustGetContainer(DEPLOYER)
   143  	configtxlator := deployment.MustGetContainer(CONFIGTXLATOR)
   144  
   145  	registryURL := instance.Spec.RegistryURL
   146  	arch := "amd64"
   147  	if instance.Spec.Arch != nil {
   148  		arch = instance.Spec.Arch[0]
   149  	}
   150  
   151  	images := &deployerimgs.ConsoleImages{}
   152  	if instance.Spec.Images != nil {
   153  		// convert spec images to deployer config images
   154  		instanceImgBytes, err := json.Marshal(instance.Spec.Images)
   155  		if err != nil {
   156  			return err
   157  		}
   158  		err = json.Unmarshal(instanceImgBytes, images)
   159  		if err != nil {
   160  			return err
   161  		}
   162  	}
   163  
   164  	var consoleImage, consoleTag, initImage, initTag, deployerImage, deployerTag string
   165  	var configtxlatorImage, configtxlatorTag, couchdbImage, couchdbTag string
   166  
   167  	defaultimage := defaultconsole.GetImages()
   168  	consoleImage = image.GetImage(registryURL, defaultimage.ConsoleImage, images.ConsoleImage)
   169  	initImage = image.GetImage(registryURL, defaultimage.ConsoleInitImage, images.ConsoleInitImage)
   170  	deployerImage = image.GetImage(registryURL, defaultimage.DeployerImage, images.DeployerImage)
   171  	configtxlatorImage = image.GetImage(registryURL, defaultimage.ConfigtxlatorImage, images.ConfigtxlatorImage)
   172  
   173  	if instance.UseTags() {
   174  		consoleTag = image.GetTag(arch, defaultimage.ConsoleTag, images.ConsoleTag)
   175  		initTag = image.GetTag(arch, defaultimage.ConsoleInitTag, images.ConsoleInitTag)
   176  		deployerTag = image.GetTag(arch, defaultimage.DeployerTag, images.DeployerTag)
   177  		configtxlatorTag = image.GetTag(arch, defaultimage.ConfigtxlatorTag, images.ConfigtxlatorTag)
   178  	} else {
   179  		consoleTag = image.GetTag(arch, defaultimage.ConsoleDigest, images.ConsoleDigest)
   180  		initTag = image.GetTag(arch, defaultimage.ConsoleInitDigest, images.ConsoleInitDigest)
   181  		deployerTag = image.GetTag(arch, defaultimage.DeployerDigest, images.DeployerDigest)
   182  		configtxlatorTag = image.GetTag(arch, defaultimage.ConfigtxlatorDigest, images.ConfigtxlatorDigest)
   183  	}
   184  	init.SetImage(initImage, initTag)
   185  	console.SetImage(consoleImage, consoleTag)
   186  	deployer.SetImage(deployerImage, deployerTag)
   187  	configtxlator.SetImage(configtxlatorImage, configtxlatorTag)
   188  
   189  	resourcesRequest := instance.Spec.Resources
   190  	if !instance.Spec.UsingRemoteDB() {
   191  		couchdb := deployment.MustGetContainer(COUCHDB)
   192  
   193  		if instance.Spec.ConnectionString != "" {
   194  			connectionURL, err := url.Parse(instance.Spec.ConnectionString)
   195  			if err != nil {
   196  				return err
   197  			}
   198  			if connectionURL.Host == "localhost:5984" {
   199  				if connectionURL.Scheme == "http" {
   200  					couchdbUser := connectionURL.User.Username()
   201  					couchdbPassword, set := connectionURL.User.Password()
   202  					if set {
   203  						couchdb.AppendEnvIfMissing("COUCHDB_USER", couchdbUser)
   204  						couchdb.AppendEnvIfMissing("COUCHDB_PASSWORD", couchdbPassword)
   205  					}
   206  				}
   207  			}
   208  		}
   209  
   210  		couchdbImage = image.GetImage(registryURL, defaultimage.CouchDBImage, images.CouchDBImage)
   211  		if instance.Spec.UseTags != nil && *(instance.Spec.UseTags) {
   212  			couchdbTag = image.GetTag(arch, defaultimage.CouchDBTag, images.CouchDBTag)
   213  		} else {
   214  			couchdbTag = image.GetTag(arch, defaultimage.CouchDBDigest, images.CouchDBDigest)
   215  
   216  		}
   217  		couchdb.SetImage(couchdbImage, couchdbTag)
   218  
   219  		if resourcesRequest != nil {
   220  			if resourcesRequest.CouchDB != nil {
   221  				err := couchdb.UpdateResources(resourcesRequest.CouchDB)
   222  				if err != nil {
   223  					return errors.Wrap(err, "update resources for couchdb failed")
   224  				}
   225  			}
   226  		}
   227  	}
   228  
   229  	if resourcesRequest != nil {
   230  		if resourcesRequest.Console != nil {
   231  			err := console.UpdateResources(resourcesRequest.Console)
   232  			if err != nil {
   233  				return errors.Wrap(err, "update resources for console failed")
   234  			}
   235  		}
   236  
   237  		if resourcesRequest.Deployer != nil {
   238  			err := deployer.UpdateResources(resourcesRequest.Deployer)
   239  			if err != nil {
   240  				return errors.Wrap(err, "update resources for deployer failed")
   241  			}
   242  		}
   243  
   244  		if resourcesRequest.Configtxlator != nil {
   245  			err := configtxlator.UpdateResources(resourcesRequest.Configtxlator)
   246  			if err != nil {
   247  				return errors.Wrap(err, "update resources for configtxlator failed")
   248  			}
   249  		}
   250  	}
   251  
   252  	if err := setReplicas(instance, deployment); err != nil {
   253  		return err
   254  	}
   255  	setDeploymentStrategy(instance, deployment)
   256  	setSpreadConstraints(instance, deployment)
   257  
   258  	kubeconfigSecretName := instance.Spec.KubeconfigSecretName
   259  	if kubeconfigSecretName != "" {
   260  		deployer.AppendVolumeMountIfMissing("kubeconfig", "/kubeconfig/")
   261  		deployment.AppendSecretVolumeIfMissing("kubeconfig", kubeconfigSecretName)
   262  		deployer.AppendEnvIfMissing("KUBECONFIGPATH", "/kubeconfig/kubeconfig.yaml")
   263  	}
   264  
   265  	kubeconfigNamespace := instance.Spec.KubeconfigNamespace
   266  	if kubeconfigNamespace != "" {
   267  		deployer.AppendEnvIfMissing("DEPLOY_NAMESPACE", kubeconfigNamespace)
   268  	} else {
   269  		valueFrom := &corev1.EnvVarSource{
   270  			FieldRef: &corev1.ObjectFieldSelector{
   271  				FieldPath: "metadata.namespace",
   272  			},
   273  		}
   274  		deployer.AppendEnvVarValueFromIfMissing("DEPLOY_NAMESPACE", valueFrom)
   275  	}
   276  
   277  	consoleOverrides, err := instance.Spec.GetOverridesConsole()
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	initCommand := ""
   283  	if !instance.Spec.UsingRemoteDB() {
   284  		initCommand = "chmod -R 775 /opt/couchdb/data/ && chown -R -H 5984:5984 /opt/couchdb/data/ && chmod -R 775 /certs/ && chown -R -H 1000:1000 /certs/"
   285  
   286  		couchDBVolumeMount := corev1.VolumeMount{
   287  			Name:      "couchdb",
   288  			MountPath: "/opt/couchdb/data",
   289  			SubPath:   "data",
   290  		}
   291  
   292  		certsVolumeMount := corev1.VolumeMount{
   293  			Name:      "couchdb",
   294  			MountPath: "/certs/",
   295  			SubPath:   "tls",
   296  		}
   297  		init.SetVolumeMounts([]corev1.VolumeMount{couchDBVolumeMount, certsVolumeMount})
   298  		console.AppendVolumeMountWithSubPathIfMissing("couchdb", "/certs/", "tls")
   299  	}
   300  
   301  	if consoleOverrides.ActivityTrackerConsolePath != "" {
   302  		hostPath := "/var/log/at"
   303  		if consoleOverrides.ActivityTrackerHostPath != "" {
   304  			hostPath = consoleOverrides.ActivityTrackerHostPath
   305  		}
   306  		deployment.AppendHostPathVolumeIfMissing("activity", hostPath, corev1.HostPathDirectoryOrCreate)
   307  
   308  		console.AppendVolumeMountWithSubPathIfMissing("activity", consoleOverrides.ActivityTrackerConsolePath, instance.Namespace)
   309  		init.AppendVolumeMountWithSubPathIfMissing("activity", consoleOverrides.ActivityTrackerConsolePath, instance.Namespace)
   310  
   311  		if initCommand != "" {
   312  			initCommand += " && "
   313  		}
   314  		initCommand += "chmod -R 775 " + consoleOverrides.ActivityTrackerConsolePath + " && chown -R -H 1000:1000 " + consoleOverrides.ActivityTrackerConsolePath
   315  	}
   316  
   317  	if initCommand == "" {
   318  		initCommand = "exit 0"
   319  	}
   320  	init.SetCommand([]string{"sh", "-c", initCommand})
   321  
   322  	return nil
   323  }
   324  
   325  func (o *Override) GetAffinity(instance *current.IBPConsole) *corev1.Affinity {
   326  	arch := instance.Spec.Arch
   327  	zone := instance.Spec.Zone
   328  	region := instance.Spec.Region
   329  	nodeSelectorTerms := common.GetNodeSelectorTerms(arch, zone, region)
   330  
   331  	affinity := &corev1.Affinity{}
   332  
   333  	if len(nodeSelectorTerms[0].MatchExpressions) != 0 {
   334  		affinity.NodeAffinity = &corev1.NodeAffinity{
   335  			RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
   336  				NodeSelectorTerms: nodeSelectorTerms,
   337  			},
   338  		}
   339  	}
   340  
   341  	return affinity
   342  }
   343  
   344  func (o *Override) CreateCouchdbContainer() container.Container {
   345  	falsep := false
   346  	truep := true
   347  	portp := int64(5984)
   348  
   349  	couchdb := &corev1.Container{
   350  		Name:            "couchdb",
   351  		Image:           "",
   352  		ImagePullPolicy: "Always",
   353  		Env: []corev1.EnvVar{
   354  			corev1.EnvVar{
   355  				Name:  "LICENSE",
   356  				Value: "accept",
   357  			},
   358  		},
   359  		SecurityContext: &corev1.SecurityContext{
   360  			Privileged:               &falsep,
   361  			AllowPrivilegeEscalation: &falsep,
   362  			ReadOnlyRootFilesystem:   &falsep,
   363  			RunAsNonRoot:             &truep,
   364  			RunAsUser:                &portp,
   365  			Capabilities: &corev1.Capabilities{
   366  				Drop: []corev1.Capability{"ALL"},
   367  				Add:  []corev1.Capability{"NET_BIND_SERVICE", "CHOWN", "DAC_OVERRIDE", "SETGID", "SETUID"},
   368  			},
   369  		},
   370  		Ports: []corev1.ContainerPort{
   371  			corev1.ContainerPort{
   372  				Name:          "http",
   373  				ContainerPort: 5984,
   374  			},
   375  		},
   376  		LivenessProbe: &corev1.Probe{
   377  			Handler: corev1.Handler{
   378  				TCPSocket: &corev1.TCPSocketAction{
   379  					Port: intstr.FromInt(5984),
   380  				},
   381  			},
   382  			InitialDelaySeconds: 16,
   383  			TimeoutSeconds:      5,
   384  			FailureThreshold:    5,
   385  		},
   386  		ReadinessProbe: &corev1.Probe{
   387  			Handler: corev1.Handler{
   388  				TCPSocket: &corev1.TCPSocketAction{
   389  					Port: intstr.FromInt(5984),
   390  				},
   391  			},
   392  			InitialDelaySeconds: 10,
   393  			TimeoutSeconds:      5,
   394  			FailureThreshold:    5,
   395  		},
   396  		Resources: corev1.ResourceRequirements{
   397  			Limits: corev1.ResourceList{
   398  				corev1.ResourceCPU:              resource.MustParse("500m"),
   399  				corev1.ResourceMemory:           resource.MustParse("1000Mi"),
   400  				corev1.ResourceEphemeralStorage: resource.MustParse("1Gi"),
   401  			},
   402  			Requests: corev1.ResourceList{
   403  				corev1.ResourceCPU:              resource.MustParse("500m"),
   404  				corev1.ResourceMemory:           resource.MustParse("1000Mi"),
   405  				corev1.ResourceEphemeralStorage: resource.MustParse("100Mi"),
   406  			},
   407  		},
   408  	}
   409  
   410  	return *container.New(couchdb)
   411  }
   412  
   413  func setReplicas(instance *current.IBPConsole, d *dep.Deployment) error {
   414  	if instance.Spec.Replicas != nil {
   415  		if !instance.Spec.UsingRemoteDB() && *instance.Spec.Replicas > 1 {
   416  			return errors.New("replicas > 1 not allowed in IBPConsole")
   417  		}
   418  
   419  		d.SetReplicas(instance.Spec.Replicas)
   420  	}
   421  
   422  	return nil
   423  }
   424  
   425  func setDeploymentStrategy(instance *current.IBPConsole, d *dep.Deployment) {
   426  	switch instance.Spec.UsingRemoteDB() {
   427  	case false:
   428  		d.Spec.Strategy = appsv1.DeploymentStrategy{
   429  			Type: appsv1.RecreateDeploymentStrategyType,
   430  		}
   431  	case true:
   432  		opts := intstr.FromString("25%")
   433  		d.Spec.Strategy = appsv1.DeploymentStrategy{
   434  			Type: appsv1.RollingUpdateDeploymentStrategyType,
   435  			RollingUpdate: &appsv1.RollingUpdateDeployment{
   436  				MaxUnavailable: &opts,
   437  				MaxSurge:       &opts,
   438  			},
   439  		}
   440  	}
   441  }
   442  
   443  func setSpreadConstraints(instance *current.IBPConsole, d *dep.Deployment) {
   444  	if instance.Spec.UsingRemoteDB() {
   445  		d.Spec.Template.Spec.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{
   446  			{
   447  				MaxSkew:           1,
   448  				TopologyKey:       "topology.kubernetes.io/zone",
   449  				WhenUnsatisfiable: corev1.ScheduleAnyway,
   450  				LabelSelector: &v1.LabelSelector{
   451  					MatchLabels: map[string]string{
   452  						"type": "ibpconsole",
   453  					},
   454  				},
   455  			},
   456  		}
   457  	}
   458  }