From 7a0bbcd5d8cde529b31e5488cf0a7cfee85e3752 Mon Sep 17 00:00:00 2001 From: Ashwathi Shiva <56001041+ashwathishiva@users.noreply.github.com> Date: Wed, 15 Apr 2020 16:17:21 -0400 Subject: [PATCH] Preflight mongo (#325) * preflight mongo check working with ca cert --- cmd/qliksense/preflight.go | 26 ++++++--- docs/command_reference.md | 3 +- docs/preflight_checks.md | 7 +-- go.mod | 1 + pkg/preflight/all_checks.go | 4 +- pkg/preflight/deployability.go | 2 +- pkg/preflight/dns_check.go | 2 +- pkg/preflight/mongo_check.go | 92 +++++++++++++++++++++++++++++--- pkg/preflight/preflight_utils.go | 81 +++++++++++++++++++++++++++- pkg/preflight/role_check.go | 5 +- 10 files changed, 196 insertions(+), 27 deletions(-) diff --git a/cmd/qliksense/preflight.go b/cmd/qliksense/preflight.go index ea34c5f..e94a18e 100644 --- a/cmd/qliksense/preflight.go +++ b/cmd/qliksense/preflight.go @@ -80,7 +80,9 @@ func pfK8sVersionCheckCmd(q *qliksense.Qliksense) *cobra.Command { } func pfAllChecksCmd(q *qliksense.Qliksense) *cobra.Command { - var mongodbUrl string + + preflightOpts := &preflight.PreflightMongoOptions{} + var preflightAllChecksCmd = &cobra.Command{ Use: "all", Short: "perform all checks", @@ -100,13 +102,19 @@ func pfAllChecksCmd(q *qliksense.Qliksense) *cobra.Command { if namespace == "" { namespace = "default" } - qp.RunAllPreflightChecks(namespace, kubeConfigContents, mongodbUrl) + qp.RunAllPreflightChecks(kubeConfigContents, namespace, preflightOpts) return nil }, } f := preflightAllChecksCmd.Flags() - f.StringVarP(&mongodbUrl, "mongodb-url", "", "", "mongodbUrl to try connecting to") + f.StringVarP(&preflightOpts.MongodbUrl, "mongodb-url", "", "", "mongodbUrl to try connecting to") + f.StringVarP(&preflightOpts.Username, "mongodb-username", "", "", "username to connect to mongodb") + f.StringVarP(&preflightOpts.Password, "mongodb-password", "", "", "password to connect to mongodb") + f.StringVarP(&preflightOpts.CaCertFile, "mongodb-ca-cert", "", "", "certificate to use for mongodb check") + f.StringVarP(&preflightOpts.ClientCertFile, "mongodb-client-cert", "", "", "client-certificate to use for mongodb check") + f.BoolVar(&preflightOpts.Tls, "mongodb-tls", false, "enable tls?") + return preflightAllChecksCmd } @@ -316,7 +324,8 @@ func pfCreateAuthCheckCmd(q *qliksense.Qliksense) *cobra.Command { } func pfMongoCheckCmd(q *qliksense.Qliksense) *cobra.Command { - var mongodbUrl string + + preflightOpts := &preflight.PreflightMongoOptions{} var preflightMongoCmd = &cobra.Command{ Use: "mongo", Short: "preflight mongo OR preflight mongo --url=", @@ -336,7 +345,7 @@ func pfMongoCheckCmd(q *qliksense.Qliksense) *cobra.Command { if namespace == "" { namespace = "default" } - if err = qp.CheckMongo(kubeConfigContents, namespace, mongodbUrl); err != nil { + if err = qp.CheckMongo(kubeConfigContents, namespace, preflightOpts); err != nil { fmt.Println(err) fmt.Print("Preflight mongo check FAILED\n") log.Fatal() @@ -345,6 +354,11 @@ func pfMongoCheckCmd(q *qliksense.Qliksense) *cobra.Command { }, } f := preflightMongoCmd.Flags() - f.StringVarP(&mongodbUrl, "url", "", "", "mongodbUrl to try connecting to") + f.StringVarP(&preflightOpts.MongodbUrl, "url", "", "", "mongodbUrl to try connecting to") + f.StringVarP(&preflightOpts.Username, "username", "", "", "username to connect to mongodb") + f.StringVarP(&preflightOpts.Password, "password", "", "", "password to connect to mongodb") + f.StringVarP(&preflightOpts.CaCertFile, "ca-cert", "", "", "ca certificate to use for mongodb check") + f.StringVarP(&preflightOpts.ClientCertFile, "client-cert", "", "", "client-certificate to use for mongodb check") + f.BoolVar(&preflightOpts.Tls, "tls", false, "enable tls?") return preflightMongoCmd } diff --git a/docs/command_reference.md b/docs/command_reference.md index b115470..2a82a13 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -4,7 +4,6 @@ Preflight checks provide pre-installation cluster conformance testing and validation before we install qliksense on the cluster. We gather a suite of conformance tests that can be easily written and run on the target cluster to verify that cluster-specific requirements are met. -The suite consists of a set of `collectors` which run the specifications of every test and `analyzers` which analyze the results of every test run by the collector. We support the following tests at the moment as part of preflight checks, and the range of the suite will be expanded in future. Run the following command to view help about the commands supported by preflight at any moment: @@ -15,7 +14,7 @@ qliksense preflight #### Running all checks Run the following command to execute all preflight checks ``` -qliksense preflight all +qliksense preflight all --mongodb-url= --mongodb-ca-cert= ``` #### Running specific check diff --git a/docs/preflight_checks.md b/docs/preflight_checks.md index 796dbc2..6c0284e 100644 --- a/docs/preflight_checks.md +++ b/docs/preflight_checks.md @@ -2,7 +2,6 @@ Preflight checks provide pre-installation cluster conformance testing and validation before we install qliksense on the cluster. We gather a suite of conformance tests that can be easily written and run on the target cluster to verify that cluster-specific requirements are met. -The suite consists of a set of `collectors` which run the specifications of every test and `analyzers` which analyze the results of every test run by the collector. We support the following tests at the moment as part of preflight checks, and the range of the suite will be expanded in future. Run the following command to view help about the commands supported by preflight at any moment: @@ -199,6 +198,8 @@ We can check if we are able to connect to an instance of mongodb on the cluster ```shell qliksense preflight mongo --url= OR qliksense preflight mongo +qliksense preflight mongo --url= --ca-cert= + Preflight mongo check --------------------- @@ -206,7 +207,7 @@ Preflight mongodb check: Created pod: pf-mongo-pod stdout: MongoDB shell version v4.2.5 connecting to: /?compressors=disabled&gssapiServiceName=mongodb -Implicit session: session { "id" : UUID("64f639d3-2c93-4894-80f6-ee14acaf56a5") } +Implicit session: session { "id" : UUID("...") } MongoDB server version: 4.2.5 bye stderr: @@ -221,7 +222,7 @@ Completed preflight mongodb check Run the command shown below to execute all preflight checks. ```shell $ qliksense preflight all --mongodb-url= OR -$ qliksense preflight all +$ qliksense preflight all --mongodb-url= --mongodb-ca-cert= Running all preflight checks diff --git a/go.mod b/go.mod index cfd0411..ebc3ca4 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( k8s.io/api v0.17.0 k8s.io/apimachinery v0.17.0 k8s.io/client-go v11.0.0+incompatible + k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51 sigs.k8s.io/kustomize/api v0.3.2 sigs.k8s.io/yaml v1.1.0 ) diff --git a/pkg/preflight/all_checks.go b/pkg/preflight/all_checks.go index afbc1ca..9058177 100644 --- a/pkg/preflight/all_checks.go +++ b/pkg/preflight/all_checks.go @@ -4,7 +4,7 @@ import ( "fmt" ) -func (qp *QliksensePreflight) RunAllPreflightChecks(namespace string, kubeConfigContents []byte, mongodbUrl string) { +func (qp *QliksensePreflight) RunAllPreflightChecks(kubeConfigContents []byte, namespace string, preflightOpts *PreflightMongoOptions) { checkCount := 0 totalCount := 0 @@ -81,7 +81,7 @@ func (qp *QliksensePreflight) RunAllPreflightChecks(namespace string, kubeConfig // Preflight mongo check fmt.Printf("\nPreflight mongo check\n") fmt.Println("---------------------") - if err := qp.CheckMongo(kubeConfigContents, namespace, mongodbUrl); err != nil { + if err := qp.CheckMongo(kubeConfigContents, namespace, preflightOpts); err != nil { fmt.Printf("Preflight mongo check: FAILED\n") } else { checkCount++ diff --git a/pkg/preflight/deployability.go b/pkg/preflight/deployability.go index 1f5f37a..0ed22f4 100644 --- a/pkg/preflight/deployability.go +++ b/pkg/preflight/deployability.go @@ -72,7 +72,7 @@ func (qp *QliksensePreflight) checkPfPod(clientset *kubernetes.Clientset, namesp if err != nil { return err } - pod, err := createPreflightTestPod(clientset, namespace, podName, imageName, commandToRun) + pod, err := createPreflightTestPod(clientset, namespace, podName, imageName, nil, commandToRun) if err != nil { err = fmt.Errorf("error: unable to create pod %s - %v\n", podName, err) return err diff --git a/pkg/preflight/dns_check.go b/pkg/preflight/dns_check.go index 90694fd..41f61da 100644 --- a/pkg/preflight/dns_check.go +++ b/pkg/preflight/dns_check.go @@ -52,7 +52,7 @@ func (qp *QliksensePreflight) CheckDns(namespace string, kubeConfigContents []by if err != nil { return err } - dnsPod, err := createPreflightTestPod(clientset, namespace, podName, netcatImageName, commandToRun) + dnsPod, err := createPreflightTestPod(clientset, namespace, podName, netcatImageName, nil, commandToRun) if err != nil { err = fmt.Errorf("error: unable to create pod : %s\n", podName) return err diff --git a/pkg/preflight/mongo_check.go b/pkg/preflight/mongo_check.go index a8c2375..f517a8b 100644 --- a/pkg/preflight/mongo_check.go +++ b/pkg/preflight/mongo_check.go @@ -2,19 +2,23 @@ package preflight import ( "fmt" + "io/ioutil" "strings" + "github.com/qlik-oss/sense-installer/pkg/api" qapi "github.com/qlik-oss/sense-installer/pkg/api" + apiv1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" ) const ( mongo = "mongo" ) -func (qp *QliksensePreflight) CheckMongo(kubeConfigContents []byte, namespace, mongodbUrl string) error { +func (qp *QliksensePreflight) CheckMongo(kubeConfigContents []byte, namespace string, preflightOpts *PreflightMongoOptions) error { fmt.Printf("Preflight mongodb check: \n") - if mongodbUrl == "" { + if preflightOpts.MongodbUrl == "" { // infer mongoDbUrl from currentCR fmt.Println("MongoDbUri is empty, infer from CR") qConfig := qapi.NewQConfig(qp.Q.QliksenseHome) @@ -28,32 +32,90 @@ func (qp *QliksensePreflight) CheckMongo(kubeConfigContents []byte, namespace, m return err } decryptedCR, err := qConfig.GetDecryptedCr(currentCR) - mongodbUrl = decryptedCR.Spec.GetFromSecrets("qliksense", "mongoDbUri") + if err != nil { + fmt.Printf("An error occurred while retrieving mongodbUrl from current CR: %v\n", err) + return err + } + preflightOpts.MongodbUrl = decryptedCR.Spec.GetFromSecrets("qliksense", "mongoDbUri") } - fmt.Printf("mongodbUrl: %s\n", mongodbUrl) - if err := qp.mongoConnCheck(kubeConfigContents, namespace, mongodbUrl); err != nil { + fmt.Printf("mongodbUrl: %s\n", preflightOpts.MongodbUrl) + if err := qp.mongoConnCheck(kubeConfigContents, namespace, preflightOpts); err != nil { return err } fmt.Println("Completed preflight mongodb check") return nil } -func (qp *QliksensePreflight) mongoConnCheck(kubeConfigContents []byte, namespace, mongodbUrl string) error { +func (qp *QliksensePreflight) mongoConnCheck(kubeConfigContents []byte, namespace string, preflightOpts *PreflightMongoOptions) error { + var caCertSecretName, clientCertSecretName string clientset, _, err := getK8SClientSet(kubeConfigContents, "") if err != nil { err = fmt.Errorf("error: unable to create a kubernetes client: %v\n", err) fmt.Println(err) return err } + var secrets []string + if preflightOpts.CaCertFile != "" { + caCertSecretName = "preflight-mongo-test-cacert" + caCertSecret, err := createSecret(clientset, namespace, preflightOpts.CaCertFile, caCertSecretName) + + if err != nil { + err = fmt.Errorf("error: unable to create a create ca cert kubernetes secret: %v\n", err) + fmt.Println(err) + return err + } + + defer deleteK8sSecret(clientset, namespace, caCertSecret) + secrets = append(secrets, caCertSecretName) + } + if preflightOpts.ClientCertFile != "" { + clientCertSecretName = "preflight-mongo-test-clientcert" + clientCertSecret, err := createSecret(clientset, namespace, preflightOpts.ClientCertFile, clientCertSecretName) + if err != nil { + err = fmt.Errorf("error: unable to create a create client cert kubernetes secret: %v\n", err) + fmt.Println(err) + return err + } + + defer deleteK8sSecret(clientset, namespace, clientCertSecret) + secrets = append(secrets, clientCertSecretName) + } + + mongoCommand := strings.Builder{} + mongoCommand.WriteString(fmt.Sprintf("sleep 10;mongo %s", preflightOpts.MongodbUrl)) + if preflightOpts.Username != "" { + mongoCommand.WriteString(fmt.Sprintf(" --username %s", preflightOpts.Username)) + api.LogDebugMessage("Adding username: Mongo command: %s\n", mongoCommand.String()) + } + if preflightOpts.Password != "" { + mongoCommand.WriteString(fmt.Sprintf(" --password %s", preflightOpts.Password)) + api.LogDebugMessage("Adding username and password\n") + } + if preflightOpts.Tls || preflightOpts.CaCertFile != "" || preflightOpts.ClientCertFile != "" { + mongoCommand.WriteString(" --tls") + api.LogDebugMessage("Adding --tls: Mongo command: %s\n", mongoCommand.String()) + } + mongoCommand.WriteString(fmt.Sprintf(" --tlsCAFile=/etc/ssl/%s/%[1]s", caCertSecretName)) + if preflightOpts.CaCertFile != "" { + api.LogDebugMessage("Adding caCertFile: Mongo command: %s\n", mongoCommand.String()) + } + if preflightOpts.ClientCertFile != "" { + mongoCommand.WriteString(fmt.Sprintf(" --tlsCertificateKeyFile=/etc/ssl/%s/%[1]s", clientCertSecretName)) + api.LogDebugMessage("Adding clientCertFile: Mongo command: %s\n", mongoCommand.String()) + } + mongoCommand.WriteString(` --eval "print(\"connected to mongo\")"`) + + commandToRun := []string{"sh", "-c", mongoCommand.String()} + api.LogDebugMessage("Mongo commandToRun: %s\n", strings.Join(commandToRun, " ")) + // create a pod podName := "pf-mongo-pod" - commandToRun := []string{"sh", "-c", "sleep 10;mongo " + mongodbUrl} imageName, err := qp.GetPreflightConfigObj().GetImageName(mongo, true) if err != nil { return err } - mongoPod, err := createPreflightTestPod(clientset, namespace, podName, imageName, commandToRun) + mongoPod, err := createPreflightTestPod(clientset, namespace, podName, imageName, secrets, commandToRun) if err != nil { err = fmt.Errorf("error: unable to create pod : %v\n", err) return err @@ -85,3 +147,17 @@ func (qp *QliksensePreflight) mongoConnCheck(kubeConfigContents []byte, namespac } return nil } + +func createSecret(clientset *kubernetes.Clientset, namespace, certFile, certSecretName string) (*apiv1.Secret, error) { + certBytes, err := ioutil.ReadFile(certFile) + if err != nil { + return nil, err + } + + certSecret, err := createPreflightTestSecret(clientset, namespace, certSecretName, certBytes) + if err != nil { + err = fmt.Errorf("error: unable to create secret with ca cert : %v\n", err) + return nil, err + } + return certSecret, nil +} diff --git a/pkg/preflight/preflight_utils.go b/pkg/preflight/preflight_utils.go index 6679891..a969a0a 100644 --- a/pkg/preflight/preflight_utils.go +++ b/pkg/preflight/preflight_utils.go @@ -26,6 +26,15 @@ import ( "k8s.io/client-go/util/retry" ) +type PreflightMongoOptions struct { + MongodbUrl string + Username string + Password string + CaCertFile string + ClientCertFile string + Tls bool +} + var gracePeriod int64 = 0 type QliksensePreflight struct { @@ -301,7 +310,7 @@ func deletePod(clientset *kubernetes.Clientset, namespace, name string) error { return nil } -func createPreflightTestPod(clientset *kubernetes.Clientset, namespace string, podName string, imageName string, commandToRun []string) (*apiv1.Pod, error) { +func createPreflightTestPod(clientset *kubernetes.Clientset, namespace, podName, imageName string, secretNames []string, commandToRun []string) (*apiv1.Pod, error) { // build the pod definition we want to deploy pod := &apiv1.Pod{ ObjectMeta: v1.ObjectMeta{ @@ -323,6 +332,31 @@ func createPreflightTestPod(clientset *kubernetes.Clientset, namespace string, p }, }, } + if len(secretNames) > 0 { + for _, secretName := range secretNames { + pod.Spec.Volumes = append(pod.Spec.Volumes, apiv1.Volume{ + Name: secretName, + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: secretName, + Items: []apiv1.KeyToPath{ + { + Key: secretName, + Path: secretName, + }, + }, + }, + }, + }) + if len(pod.Spec.Containers) > 0 { + pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, apiv1.VolumeMount{ + Name: secretName, + MountPath: "/etc/ssl/" + secretName, + ReadOnly: true, + }) + } + } + } // now create the pod in kubernetes cluster using the clientset if err := retryOnError(func() (err error) { @@ -561,7 +595,7 @@ func createPfRoleBinding(clientset *kubernetes.Clientset, namespace, roleBinding Name: roleBindingName, Namespace: namespace, Labels: map[string]string{ - "app": "demo", + "app": "preflight", }, }, Subjects: []v1beta1.Subject{ @@ -643,3 +677,46 @@ func deleteServiceAccount(clientset *kubernetes.Clientset, namespace string, ser } fmt.Printf("Deleted ServiceAccount: %s\n\n", serviceAccount.Name) } + +func createPreflightTestSecret(clientset *kubernetes.Clientset, namespace, secretName string, secretData []byte) (*apiv1.Secret, error) { + var secret *apiv1.Secret + var err error + // build the secret defination we want to create + secretSpec := &apiv1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Labels: map[string]string{ + "app": "preflight", + }, + }, + Data: map[string][]byte{ + secretName: secretData, + }, + } + + // now create the secret in kubernetes cluster using the clientset + if err = retryOnError(func() (err error) { + secret, err = clientset.CoreV1().Secrets(namespace).Create(secretSpec) + return err + }); err != nil { + fmt.Println(err) + return nil, err + } + fmt.Printf("Created Secret: %s\n", secret.Name) + return secret, nil +} + +func deleteK8sSecret(clientset *kubernetes.Clientset, namespace string, k8sSecret *apiv1.Secret) { + secretClient := clientset.CoreV1().Secrets(namespace) + + deletePolicy := v1.DeletePropagationForeground + deleteOptions := v1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + } + err := secretClient.Delete(k8sSecret.GetName(), &deleteOptions) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Deleted Secret: %s\n\n", k8sSecret.Name) +} diff --git a/pkg/preflight/role_check.go b/pkg/preflight/role_check.go index 63f5139..ad3f7f1 100644 --- a/pkg/preflight/role_check.go +++ b/pkg/preflight/role_check.go @@ -72,7 +72,8 @@ func (qp *QliksensePreflight) checkCreateEntity(namespace, entityToTest string) if len(resultYamlBytes) == 0 { resultYamlBytes, err = qliksense.ExecuteKustomizeBuild(kusDir) if err != nil { - fmt.Printf("Unable to retrieve manifests from executing kustomize: %v\n", err) + err := fmt.Errorf("Unable to retrieve manifests from executing kustomize from dir: %s", kusDir) + fmt.Println(err) return err } } @@ -80,7 +81,7 @@ func (qp *QliksensePreflight) checkCreateEntity(namespace, entityToTest string) if sa != "" { sa = strings.Replace(sa, "name: qliksense", "name: preflight", -1) } else { - err := fmt.Errorf("Unable to retrieve yamls to apply on cluster") + err := fmt.Errorf("Unable to retrieve yamls to apply on cluster from dir: %s", kusDir) fmt.Println(err) return err }