github.com/mkimuram/operator-sdk@v0.7.1-0.20190410172100-52ad33a4bda0/internal/pkg/scorecard/resource_handler.go (about)

     1  // Copyright 2019 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package scorecard
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"time"
    25  
    26  	"github.com/operator-framework/operator-sdk/internal/util/yamlutil"
    27  	proxyConf "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig"
    28  	"github.com/operator-framework/operator-sdk/pkg/k8sutil"
    29  
    30  	"github.com/ghodss/yaml"
    31  	log "github.com/sirupsen/logrus"
    32  	"github.com/spf13/viper"
    33  	appsv1 "k8s.io/api/apps/v1"
    34  	v1 "k8s.io/api/core/v1"
    35  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    38  	"k8s.io/apimachinery/pkg/labels"
    39  	"k8s.io/apimachinery/pkg/runtime"
    40  	"k8s.io/apimachinery/pkg/types"
    41  	"k8s.io/apimachinery/pkg/util/wait"
    42  	"k8s.io/client-go/kubernetes"
    43  	"sigs.k8s.io/controller-runtime/pkg/client"
    44  )
    45  
    46  type cleanupFn func() error
    47  
    48  // waitUntilCRStatusExists waits until the status block of the CR currently being tested exists. If the timeout
    49  // is reached, it simply continues and assumes there is no status block
    50  func waitUntilCRStatusExists(cr *unstructured.Unstructured) error {
    51  	err := wait.Poll(time.Second*1, time.Second*time.Duration(viper.GetInt(InitTimeoutOpt)), func() (bool, error) {
    52  		err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: cr.GetNamespace(), Name: cr.GetName()}, cr)
    53  		if err != nil {
    54  			return false, fmt.Errorf("error getting custom resource: %v", err)
    55  		}
    56  		if cr.Object["status"] != nil {
    57  			return true, nil
    58  		}
    59  		return false, nil
    60  	})
    61  	if err != nil && err != wait.ErrWaitTimeout {
    62  		return err
    63  	}
    64  	return nil
    65  }
    66  
    67  // yamlToUnstructured decodes a yaml file into an unstructured object
    68  func yamlToUnstructured(yamlPath string) (*unstructured.Unstructured, error) {
    69  	yamlFile, err := ioutil.ReadFile(yamlPath)
    70  	if err != nil {
    71  		return nil, fmt.Errorf("failed to read file %s: %v", yamlPath, err)
    72  	}
    73  	if bytes.Contains(yamlFile, []byte("\n---\n")) {
    74  		return nil, fmt.Errorf("custom resource manifest cannot have more than 1 resource")
    75  	}
    76  	obj := &unstructured.Unstructured{}
    77  	jsonSpec, err := yaml.YAMLToJSON(yamlFile)
    78  	if err != nil {
    79  		return nil, fmt.Errorf("could not convert yaml file to json: %v", err)
    80  	}
    81  	if err := obj.UnmarshalJSON(jsonSpec); err != nil {
    82  		return nil, fmt.Errorf("failed to unmarshal custom resource manifest to unstructured: %s", err)
    83  	}
    84  	// set the namespace
    85  	obj.SetNamespace(viper.GetString(NamespaceOpt))
    86  	return obj, nil
    87  }
    88  
    89  // createFromYAMLFile will take a path to a YAML file and create the resource. If it finds a
    90  // deployment, it will add the scorecard proxy as a container in the deployments podspec.
    91  func createFromYAMLFile(yamlPath string) error {
    92  	yamlSpecs, err := ioutil.ReadFile(yamlPath)
    93  	if err != nil {
    94  		return fmt.Errorf("failed to read file %s: %v", yamlPath, err)
    95  	}
    96  	scanner := yamlutil.NewYAMLScanner(yamlSpecs)
    97  	for scanner.Scan() {
    98  		obj := &unstructured.Unstructured{}
    99  		jsonSpec, err := yaml.YAMLToJSON(scanner.Bytes())
   100  		if err != nil {
   101  			return fmt.Errorf("could not convert yaml file to json: %v", err)
   102  		}
   103  		if err := obj.UnmarshalJSON(jsonSpec); err != nil {
   104  			return fmt.Errorf("could not unmarshal resource spec: %v", err)
   105  		}
   106  		obj.SetNamespace(viper.GetString(NamespaceOpt))
   107  
   108  		// dirty hack to merge scorecard proxy into operator deployment; lots of serialization and deserialization
   109  		if obj.GetKind() == "Deployment" {
   110  			// TODO: support multiple deployments
   111  			if deploymentName != "" {
   112  				return fmt.Errorf("scorecard currently does not support multiple deployments in the manifests")
   113  			}
   114  			dep, err := unstructuredToDeployment(obj)
   115  			if err != nil {
   116  				return fmt.Errorf("failed to convert object to deployment: %v", err)
   117  			}
   118  			deploymentName = dep.GetName()
   119  			err = createKubeconfigSecret()
   120  			if err != nil {
   121  				return fmt.Errorf("failed to create kubeconfig secret for scorecard-proxy: %v", err)
   122  			}
   123  			addMountKubeconfigSecret(dep)
   124  			addProxyContainer(dep)
   125  			// go back to unstructured to create
   126  			obj, err = deploymentToUnstructured(dep)
   127  			if err != nil {
   128  				return fmt.Errorf("failed to convert deployment to unstructured: %v", err)
   129  			}
   130  		}
   131  		err = runtimeClient.Create(context.TODO(), obj)
   132  		if err != nil {
   133  			_, restErr := restMapper.RESTMappings(obj.GetObjectKind().GroupVersionKind().GroupKind())
   134  			if restErr == nil {
   135  				return err
   136  			}
   137  			// don't store error, as only error will be timeout. Error from runtime client will be easier for
   138  			// the user to understand than the timeout error, so just use that if we fail
   139  			_ = wait.PollImmediate(time.Second*1, time.Second*10, func() (bool, error) {
   140  				restMapper.Reset()
   141  				_, err := restMapper.RESTMappings(obj.GetObjectKind().GroupVersionKind().GroupKind())
   142  				if err != nil {
   143  					return false, nil
   144  				}
   145  				return true, nil
   146  			})
   147  			err = runtimeClient.Create(context.TODO(), obj)
   148  			if err != nil {
   149  				return err
   150  			}
   151  		}
   152  		addResourceCleanup(obj, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()})
   153  		if obj.GetKind() == "Deployment" {
   154  			proxyPodGlobal, err = getPodFromDeployment(deploymentName, viper.GetString(NamespaceOpt))
   155  			if err != nil {
   156  				return err
   157  			}
   158  		}
   159  	}
   160  	if err := scanner.Err(); err != nil {
   161  		return fmt.Errorf("failed to scan %s: (%v)", yamlPath, err)
   162  	}
   163  
   164  	return nil
   165  }
   166  
   167  // getPodFromDeployment returns a deployment depName's pod in namespace.
   168  func getPodFromDeployment(depName, namespace string) (pod *v1.Pod, err error) {
   169  	dep := &appsv1.Deployment{}
   170  	err = runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: depName}, dep)
   171  	if err != nil {
   172  		return nil, fmt.Errorf("failed to get newly created deployment: %v", err)
   173  	}
   174  	set := labels.Set(dep.Spec.Selector.MatchLabels)
   175  	// In some cases, the pod from the old deployment will be picked up
   176  	// instead of the new one.
   177  	err = wait.PollImmediate(time.Second*1, time.Second*60, func() (bool, error) {
   178  		pods := &v1.PodList{}
   179  		err = runtimeClient.List(context.TODO(), &client.ListOptions{LabelSelector: set.AsSelector()}, pods)
   180  		if err != nil {
   181  			return false, fmt.Errorf("failed to get list of pods in deployment: %v", err)
   182  		}
   183  		// Make sure the pods exist. There should only be 1 pod per deployment.
   184  		if len(pods.Items) == 1 {
   185  			// If the pod has a deletion timestamp, it is the old pod; wait for
   186  			// pod with no deletion timestamp
   187  			if pods.Items[0].GetDeletionTimestamp() == nil {
   188  				pod = &pods.Items[0]
   189  				return true, nil
   190  			}
   191  		} else {
   192  			log.Debug("Operator deployment has more than 1 pod")
   193  		}
   194  		return false, nil
   195  	})
   196  	if err != nil {
   197  		return nil, fmt.Errorf("failed to get proxyPod: %s", err)
   198  	}
   199  	return pod, nil
   200  }
   201  
   202  // createKubeconfigSecret creates the secret that will be mounted in the operator's container and contains
   203  // the kubeconfig for communicating with the proxy
   204  func createKubeconfigSecret() error {
   205  	kubeconfigMap := make(map[string][]byte)
   206  	kc, err := proxyConf.Create(metav1.OwnerReference{Name: "scorecard"}, "http://localhost:8889", viper.GetString(NamespaceOpt))
   207  	if err != nil {
   208  		return err
   209  	}
   210  	defer func() {
   211  		if err := os.Remove(kc.Name()); err != nil {
   212  			log.Errorf("Failed to delete generated kubeconfig file: (%v)", err)
   213  		}
   214  	}()
   215  	kc, err = os.Open(kc.Name())
   216  	if err != nil {
   217  		return err
   218  	}
   219  	kcBytes, err := ioutil.ReadAll(kc)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	kubeconfigMap["kubeconfig"] = kcBytes
   224  	kubeconfigSecret := &v1.Secret{
   225  		ObjectMeta: metav1.ObjectMeta{
   226  			Name:      "scorecard-kubeconfig",
   227  			Namespace: viper.GetString(NamespaceOpt),
   228  		},
   229  		Data: kubeconfigMap,
   230  	}
   231  	err = runtimeClient.Create(context.TODO(), kubeconfigSecret)
   232  	if err != nil {
   233  		return err
   234  	}
   235  	addResourceCleanup(kubeconfigSecret, types.NamespacedName{Namespace: kubeconfigSecret.GetNamespace(), Name: kubeconfigSecret.GetName()})
   236  	return nil
   237  }
   238  
   239  // addMountKubeconfigSecret creates the volume mount for the kubeconfig secret
   240  func addMountKubeconfigSecret(dep *appsv1.Deployment) {
   241  	// create mount for secret
   242  	dep.Spec.Template.Spec.Volumes = append(dep.Spec.Template.Spec.Volumes, v1.Volume{
   243  		Name: "scorecard-kubeconfig",
   244  		VolumeSource: v1.VolumeSource{Secret: &v1.SecretVolumeSource{
   245  			SecretName: "scorecard-kubeconfig",
   246  			Items: []v1.KeyToPath{{
   247  				Key:  "kubeconfig",
   248  				Path: "config",
   249  			}},
   250  		},
   251  		},
   252  	})
   253  	for index := range dep.Spec.Template.Spec.Containers {
   254  		// mount the volume
   255  		dep.Spec.Template.Spec.Containers[index].VolumeMounts = append(dep.Spec.Template.Spec.Containers[index].VolumeMounts, v1.VolumeMount{
   256  			Name:      "scorecard-kubeconfig",
   257  			MountPath: "/scorecard-secret",
   258  		})
   259  		// specify the path via KUBECONFIG env var
   260  		dep.Spec.Template.Spec.Containers[index].Env = append(dep.Spec.Template.Spec.Containers[index].Env, v1.EnvVar{
   261  			Name:  "KUBECONFIG",
   262  			Value: "/scorecard-secret/config",
   263  		})
   264  	}
   265  }
   266  
   267  // addProxyContainer adds the container spec for the scorecard-proxy to the deployment's podspec
   268  func addProxyContainer(dep *appsv1.Deployment) {
   269  	pullPolicyString := viper.GetString(ProxyPullPolicyOpt)
   270  	var pullPolicy v1.PullPolicy
   271  	switch pullPolicyString {
   272  	case "Always":
   273  		pullPolicy = v1.PullAlways
   274  	case "Never":
   275  		pullPolicy = v1.PullNever
   276  	case "PullIfNotPresent":
   277  		pullPolicy = v1.PullIfNotPresent
   278  	default:
   279  		// this case shouldn't happen since we check the values in scorecard.go, but just in case, we'll default to always to prevent errors
   280  		pullPolicy = v1.PullAlways
   281  	}
   282  	dep.Spec.Template.Spec.Containers = append(dep.Spec.Template.Spec.Containers, v1.Container{
   283  		Name:            scorecardContainerName,
   284  		Image:           viper.GetString(ProxyImageOpt),
   285  		ImagePullPolicy: pullPolicy,
   286  		Command:         []string{"scorecard-proxy"},
   287  		Env: []v1.EnvVar{{
   288  			Name:      k8sutil.WatchNamespaceEnvVar,
   289  			ValueFrom: &v1.EnvVarSource{FieldRef: &v1.ObjectFieldSelector{FieldPath: "metadata.namespace"}},
   290  		}},
   291  	})
   292  }
   293  
   294  // unstructuredToDeployment converts an unstructured object to a deployment
   295  func unstructuredToDeployment(obj *unstructured.Unstructured) (*appsv1.Deployment, error) {
   296  	jsonByte, err := obj.MarshalJSON()
   297  	if err != nil {
   298  		return nil, fmt.Errorf("failed to convert deployment to json: %v", err)
   299  	}
   300  	depObj, _, err := dynamicDecoder.Decode(jsonByte, nil, nil)
   301  	if err != nil {
   302  		return nil, fmt.Errorf("failed to decode deployment object: %v", err)
   303  	}
   304  	switch o := depObj.(type) {
   305  	case *appsv1.Deployment:
   306  		return o, nil
   307  	default:
   308  		return nil, fmt.Errorf("conversion of runtime object to deployment failed (resulting runtime object not deployment type)")
   309  	}
   310  }
   311  
   312  // deploymentToUnstructured converts a deployment to an unstructured object
   313  func deploymentToUnstructured(dep *appsv1.Deployment) (*unstructured.Unstructured, error) {
   314  	jsonByte, err := json.Marshal(dep)
   315  	if err != nil {
   316  		return nil, fmt.Errorf("failed to remarshal deployment: %v", err)
   317  	}
   318  	obj := &unstructured.Unstructured{}
   319  	err = obj.UnmarshalJSON(jsonByte)
   320  	if err != nil {
   321  		return nil, fmt.Errorf("failed to unmarshal updated deployment: %v", err)
   322  	}
   323  	return obj, nil
   324  }
   325  
   326  // cleanupScorecard runs all cleanup functions in reverse order
   327  func cleanupScorecard() error {
   328  	failed := false
   329  	for i := len(cleanupFns) - 1; i >= 0; i-- {
   330  		err := cleanupFns[i]()
   331  		if err != nil {
   332  			failed = true
   333  			log.Printf("a cleanup function failed with error: %v\n", err)
   334  		}
   335  	}
   336  	if failed {
   337  		return fmt.Errorf("a cleanup function failed; see stdout for more details")
   338  	}
   339  	return nil
   340  }
   341  
   342  // addResourceCleanup adds a cleanup function for the specified runtime object
   343  func addResourceCleanup(obj runtime.Object, key types.NamespacedName) {
   344  	cleanupFns = append(cleanupFns, func() error {
   345  		// make a copy of the object because the client changes it
   346  		objCopy := obj.DeepCopyObject()
   347  		err := runtimeClient.Delete(context.TODO(), obj)
   348  		if err != nil {
   349  			return err
   350  		}
   351  		err = wait.PollImmediate(time.Second*1, time.Second*10, func() (bool, error) {
   352  			err = runtimeClient.Get(context.TODO(), key, objCopy)
   353  			if err != nil {
   354  				if apierrors.IsNotFound(err) {
   355  					return true, nil
   356  				}
   357  				return false, fmt.Errorf("error encountered during deletion of resource type %v with namespace/name (%+v): %v", objCopy.GetObjectKind().GroupVersionKind().Kind, key, err)
   358  			}
   359  			return false, nil
   360  		})
   361  		if err != nil {
   362  			return fmt.Errorf("cleanup function failed: %v", err)
   363  		}
   364  		return nil
   365  	})
   366  }
   367  
   368  func getProxyLogs(proxyPod *v1.Pod) (string, error) {
   369  	// need a standard kubeclient for pod logs
   370  	kubeclient, err := kubernetes.NewForConfig(kubeconfig)
   371  	if err != nil {
   372  		return "", fmt.Errorf("failed to create kubeclient: %v", err)
   373  	}
   374  	logOpts := &v1.PodLogOptions{Container: scorecardContainerName}
   375  	req := kubeclient.CoreV1().Pods(proxyPod.GetNamespace()).GetLogs(proxyPod.GetName(), logOpts)
   376  	readCloser, err := req.Stream()
   377  	if err != nil {
   378  		return "", fmt.Errorf("failed to get logs: %v", err)
   379  	}
   380  	defer readCloser.Close()
   381  	buf := new(bytes.Buffer)
   382  	_, err = buf.ReadFrom(readCloser)
   383  	if err != nil {
   384  		return "", fmt.Errorf("test failed and failed to read pod logs: %v", err)
   385  	}
   386  	return buf.String(), nil
   387  }