github.com/IBM-Blockchain/fabric-operator@v1.0.4/integration/actions/ca/ca_test.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  package ca_test
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"crypto/ecdsa"
    24  	"crypto/elliptic"
    25  	"crypto/rand"
    26  	"crypto/x509"
    27  	"crypto/x509/pkix"
    28  	"encoding/base64"
    29  	"encoding/json"
    30  	"encoding/pem"
    31  	"fmt"
    32  	"math/big"
    33  	"time"
    34  
    35  	current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1"
    36  	"github.com/IBM-Blockchain/fabric-operator/integration"
    37  	"github.com/IBM-Blockchain/fabric-operator/integration/helper"
    38  	v1 "github.com/IBM-Blockchain/fabric-operator/pkg/apis/ca/v1"
    39  	"github.com/IBM-Blockchain/fabric-operator/pkg/offering/common"
    40  	"github.com/IBM-Blockchain/fabric-operator/pkg/util"
    41  	"github.com/IBM-Blockchain/fabric-operator/pkg/util/pointer"
    42  	. "github.com/onsi/ginkgo/v2"
    43  	. "github.com/onsi/gomega"
    44  	"github.com/pkg/errors"
    45  	corev1 "k8s.io/api/core/v1"
    46  	"k8s.io/apimachinery/pkg/api/resource"
    47  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    48  	"k8s.io/apimachinery/pkg/runtime"
    49  	"sigs.k8s.io/controller-runtime/pkg/client"
    50  )
    51  
    52  var _ = Describe("trigger CA actions", func() {
    53  	AfterEach(func() {
    54  		// Set flag if a test falls
    55  		if CurrentGinkgoTestDescription().Failed {
    56  			testFailed = true
    57  		}
    58  	})
    59  
    60  	Context("renew TLS cert set to true", func() {
    61  		var (
    62  			expiringCA *helper.CA
    63  			ibpca      *current.IBPCA
    64  		)
    65  
    66  		Context("TLS certificate", func() {
    67  			var (
    68  				err       error
    69  				cert, key []byte
    70  			)
    71  
    72  			BeforeEach(func() {
    73  				key, cert, err = GenSelfSignedCert(time.Hour * 48)
    74  				Expect(err).NotTo(HaveOccurred())
    75  
    76  				certB64 := util.BytesToBase64(cert)
    77  				keyB64 := util.BytesToBase64(key)
    78  
    79  				override := &v1.ServerConfig{
    80  					TLS: v1.ServerTLSConfig{
    81  						Enabled:  pointer.True(),
    82  						CertFile: certB64,
    83  						KeyFile:  keyB64,
    84  					},
    85  				}
    86  				overrideBytes, err := json.Marshal(override)
    87  				Expect(err).NotTo(HaveOccurred())
    88  
    89  				expiringCA = CAWithOverrides(json.RawMessage(overrideBytes))
    90  				helper.CreateCA(ibpCRClient, expiringCA.CR)
    91  
    92  				Eventually(expiringCA.PodIsRunning).Should((Equal(true)))
    93  			})
    94  
    95  			When("TLS cert renew action is set to false", func() {
    96  				BeforeEach(func() {
    97  					patch := func(o client.Object) {
    98  						ibpca = o.(*current.IBPCA)
    99  						ibpca.Spec.Action.Renew.TLSCert = true
   100  					}
   101  
   102  					err := integration.ResilientPatch(ibpCRClient, expiringCA.Name, namespace, IBPCAS, 3, &current.IBPCA{}, patch)
   103  					Expect(err).NotTo(HaveOccurred())
   104  
   105  					Eventually(expiringCA.PodIsRunning).Should((Equal(true)))
   106  				})
   107  
   108  				It("renews", func() {
   109  					By("backing up old crypto", func() {
   110  						Eventually(func() bool {
   111  							backup, err := GetBackup("tls", expiringCA.CR.Name)
   112  							if err != nil {
   113  								return false
   114  							}
   115  
   116  							if len(backup.List) > 0 {
   117  								return backup.List[len(backup.List)-1].SignCerts == base64.StdEncoding.EncodeToString(cert)
   118  							}
   119  
   120  							return false
   121  						}).Should(Equal(true))
   122  					})
   123  
   124  					By("updating crypto secret with new TLS Cert", func() {
   125  						Eventually(func() bool {
   126  							crypto, err := kclient.CoreV1().Secrets(namespace).
   127  								Get(context.TODO(), fmt.Sprintf("%s-ca-crypto", expiringCA.CR.Name), metav1.GetOptions{})
   128  							Expect(err).NotTo(HaveOccurred())
   129  
   130  							return bytes.Equal(cert, crypto.Data["tls-cert.pem"])
   131  						}).Should(Equal(false))
   132  					})
   133  
   134  					By("updating operations cert to match new TLS cert", func() {
   135  						crypto, err := kclient.CoreV1().Secrets(namespace).
   136  							Get(context.TODO(), fmt.Sprintf("%s-ca-crypto", expiringCA.CR.Name), metav1.GetOptions{})
   137  						Expect(err).NotTo(HaveOccurred())
   138  
   139  						Expect(bytes.Equal(
   140  							crypto.Data["operations-cert.pem"],
   141  							crypto.Data["tls-cert.pem"],
   142  						)).To(Equal(true))
   143  					})
   144  
   145  					By("refreshing the TLS certificate with expiration value of plus 10 years", func() {
   146  						crypto, err := kclient.CoreV1().Secrets(namespace).
   147  							Get(context.TODO(), fmt.Sprintf("%s-ca-crypto", expiringCA.CR.Name), metav1.GetOptions{})
   148  						Expect(err).NotTo(HaveOccurred())
   149  
   150  						newTLSCert := crypto.Data["tls-cert.pem"]
   151  						newCert, err := util.GetCertificateFromPEMBytes(newTLSCert)
   152  						Expect(err).NotTo(HaveOccurred())
   153  						Expect(newCert.NotAfter.Year()).To(Equal(time.Now().Add(time.Hour * 87600).Year()))
   154  					})
   155  
   156  					By("updating crypto secret with new TLS Key", func() {
   157  						Eventually(func() bool {
   158  							crypto, err := kclient.CoreV1().Secrets(namespace).
   159  								Get(context.TODO(), fmt.Sprintf("%s-ca-crypto", expiringCA.CR.Name), metav1.GetOptions{})
   160  							Expect(err).NotTo(HaveOccurred())
   161  
   162  							return bytes.Equal(key, crypto.Data["tls-key.pem"])
   163  						}).Should(Equal(false))
   164  					})
   165  
   166  					By("updating operations key to match new TLS Key", func() {
   167  						crypto, err := kclient.CoreV1().Secrets(namespace).
   168  							Get(context.TODO(), fmt.Sprintf("%s-ca-crypto", expiringCA.CR.Name), metav1.GetOptions{})
   169  						Expect(err).NotTo(HaveOccurred())
   170  
   171  						Expect(bytes.Equal(
   172  							crypto.Data["operations-key.pem"],
   173  							crypto.Data["tls-key.pem"],
   174  						)).To(Equal(true))
   175  					})
   176  
   177  					By("updating connection profile with new TLS cert", func() {
   178  						Eventually(func() bool {
   179  							cm, err := kclient.CoreV1().
   180  								ConfigMaps(namespace).
   181  								Get(context.TODO(),
   182  									fmt.Sprintf("%s-connection-profile", expiringCA.CR.Name),
   183  									metav1.GetOptions{},
   184  								)
   185  							Expect(err).NotTo(HaveOccurred())
   186  
   187  							profileBytes := cm.BinaryData["profile.json"]
   188  							connectionProfile := &current.CAConnectionProfile{}
   189  							err = json.Unmarshal(profileBytes, connectionProfile)
   190  							Expect(err).NotTo(HaveOccurred())
   191  
   192  							crypto, err := kclient.CoreV1().Secrets(namespace).
   193  								Get(context.TODO(), fmt.Sprintf("%s-ca-crypto", expiringCA.CR.Name), metav1.GetOptions{})
   194  							Expect(err).NotTo(HaveOccurred())
   195  
   196  							return bytes.Equal([]byte(connectionProfile.TLS.Cert), crypto.Data["tls-key.pem"])
   197  						}).Should(Equal(false))
   198  					})
   199  
   200  					By("setting restart flag back to false after restart", func() {
   201  						Eventually(func() bool {
   202  							result := ibpCRClient.Get().Namespace(namespace).Resource(IBPCAS).Name(expiringCA.Name).Do(context.TODO())
   203  							ibpca := &current.IBPCA{}
   204  							result.Into(ibpca)
   205  
   206  							return ibpca.Spec.Action.Renew.TLSCert
   207  						}).Should(Equal(false))
   208  					})
   209  				})
   210  			})
   211  		})
   212  	})
   213  
   214  	Context("restart", func() {
   215  		var (
   216  			podName string
   217  			ca      *current.IBPCA
   218  		)
   219  
   220  		BeforeEach(func() {
   221  			Eventually(func() int {
   222  				return len(org1ca.GetPods())
   223  			}).Should(Equal(1))
   224  
   225  			podName = org1ca.GetPods()[0].Name
   226  
   227  			result := ibpCRClient.Get().Namespace(namespace).Resource(IBPCAS).Name(org1ca.Name).Do(context.TODO())
   228  			Expect(result.Error()).NotTo(HaveOccurred())
   229  
   230  			ca = &current.IBPCA{}
   231  			result.Into(ca)
   232  		})
   233  
   234  		When("spec has restart flag set to true", func() {
   235  			BeforeEach(func() {
   236  				ca.Spec.Action.Restart = true
   237  			})
   238  
   239  			It("performs restart action", func() {
   240  				bytes, err := json.Marshal(ca)
   241  				Expect(err).NotTo(HaveOccurred())
   242  
   243  				result := ibpCRClient.Put().Namespace(namespace).Resource(IBPCAS).Name(org1ca.Name).Body(bytes).Do(context.TODO())
   244  				Expect(result.Error()).NotTo(HaveOccurred())
   245  
   246  				Eventually(org1ca.PodIsRunning).Should((Equal(true)))
   247  
   248  				By("restarting ca pod", func() {
   249  					Eventually(func() bool {
   250  						pods := org1ca.GetPods()
   251  						if len(pods) == 0 {
   252  							return false
   253  						}
   254  
   255  						newPodName := pods[0].Name
   256  						if newPodName != podName {
   257  							return true
   258  						}
   259  
   260  						return false
   261  					}).Should(Equal(true))
   262  				})
   263  
   264  				By("setting restart flag back to false after restart", func() {
   265  					Eventually(func() bool {
   266  						result := ibpCRClient.Get().Namespace(namespace).Resource(IBPCAS).Name(org1ca.Name).Do(context.TODO())
   267  						ca := &current.IBPCA{}
   268  						result.Into(ca)
   269  
   270  						return ca.Spec.Action.Restart
   271  					}).Should(Equal(false))
   272  				})
   273  			})
   274  		})
   275  	})
   276  
   277  })
   278  
   279  func CAWithOverrides(rawMessage json.RawMessage) *helper.CA {
   280  	cr := &current.IBPCA{
   281  		ObjectMeta: metav1.ObjectMeta{
   282  			Name:      "org2ca",
   283  			Namespace: namespace,
   284  		},
   285  		Spec: current.IBPCASpec{
   286  			License: current.License{
   287  				Accept: true,
   288  			},
   289  			ImagePullSecrets: []string{"regcred"},
   290  			Images: &current.CAImages{
   291  				CAImage:     integration.CaImage,
   292  				CATag:       integration.CaTag,
   293  				CAInitImage: integration.InitImage,
   294  				CAInitTag:   integration.InitTag,
   295  			},
   296  			Resources: &current.CAResources{
   297  				CA: &corev1.ResourceRequirements{
   298  					Requests: corev1.ResourceList{
   299  						corev1.ResourceCPU:              resource.MustParse("50m"),
   300  						corev1.ResourceMemory:           resource.MustParse("100M"),
   301  						corev1.ResourceEphemeralStorage: resource.MustParse("100M"),
   302  					},
   303  					Limits: corev1.ResourceList{
   304  						corev1.ResourceCPU:              resource.MustParse("50m"),
   305  						corev1.ResourceMemory:           resource.MustParse("100M"),
   306  						corev1.ResourceEphemeralStorage: resource.MustParse("1G"),
   307  					},
   308  				},
   309  			},
   310  			Zone:   "select",
   311  			Region: "select",
   312  			Domain: domain,
   313  			ConfigOverride: &current.ConfigOverride{
   314  				CA: &runtime.RawExtension{Raw: rawMessage},
   315  			},
   316  			FabricVersion: integration.FabricCAVersion,
   317  		},
   318  	}
   319  
   320  	return &helper.CA{
   321  		Domain:     domain,
   322  		Name:       cr.Name,
   323  		Namespace:  namespace,
   324  		WorkingDir: wd,
   325  		CR:         cr,
   326  		CRClient:   ibpCRClient,
   327  		KClient:    kclient,
   328  		NativeResourcePoller: integration.NativeResourcePoller{
   329  			Name:      cr.Name,
   330  			Namespace: namespace,
   331  			Client:    kclient,
   332  		},
   333  	}
   334  }
   335  
   336  // Generate TLS cert that is expires in the x days
   337  func GenSelfSignedCert(expiresIn time.Duration) ([]byte, []byte, error) {
   338  	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   339  	if err != nil {
   340  		return nil, nil, errors.Wrap(err, "failed to generate key")
   341  	}
   342  
   343  	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
   344  	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
   345  	if err != nil {
   346  		return nil, nil, errors.Wrap(err, "failed to generate serial number")
   347  	}
   348  
   349  	notBefore := time.Now()
   350  	notAfter := notBefore.Add(expiresIn)
   351  
   352  	template := x509.Certificate{
   353  		SerialNumber: serialNumber,
   354  		Issuer: pkix.Name{
   355  			Country:            []string{"US"},
   356  			Province:           []string{"North Carolina"},
   357  			Locality:           []string{"Durham"},
   358  			Organization:       []string{"IBM"},
   359  			OrganizationalUnit: []string{"Blockchain"},
   360  		},
   361  		Subject: pkix.Name{
   362  			Country:            []string{"US"},
   363  			Province:           []string{"North Carolina"},
   364  			Locality:           []string{"Durham"},
   365  			Organization:       []string{"IBM"},
   366  			OrganizationalUnit: []string{"Blockchain"},
   367  		},
   368  		NotBefore: notBefore,
   369  		NotAfter:  notAfter,
   370  	}
   371  
   372  	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
   373  	if err != nil {
   374  		return nil, nil, errors.Wrap(err, "failed to create certificate")
   375  	}
   376  
   377  	keyBytes, err := x509.MarshalECPrivateKey(priv)
   378  	if err != nil {
   379  		return nil, nil, errors.Wrap(err, "failed to marshal key")
   380  	}
   381  
   382  	certPEM := &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}
   383  	keyPEM := &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
   384  
   385  	certBytes := pem.EncodeToMemory(certPEM)
   386  	keyBytes = pem.EncodeToMemory(keyPEM)
   387  
   388  	return keyBytes, certBytes, nil
   389  }
   390  
   391  func GetBackup(certType, name string) (*common.Backup, error) {
   392  	backupSecret, err := kclient.CoreV1().Secrets(namespace).Get(context.TODO(), fmt.Sprintf("%s-crypto-backup", name), metav1.GetOptions{})
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  
   397  	backup := &common.Backup{}
   398  	key := fmt.Sprintf("%s-backup.json", certType)
   399  	err = json.Unmarshal(backupSecret.Data[key], backup)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  
   404  	return backup, nil
   405  }