github.com/jmrodri/operator-sdk@v0.5.0/commands/operator-sdk/cmd/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  	proxyConf "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig"
    27  	"github.com/operator-framework/operator-sdk/pkg/k8sutil"
    28  	"github.com/spf13/viper"
    29  
    30  	"github.com/ghodss/yaml"
    31  	log "github.com/sirupsen/logrus"
    32  	appsv1 "k8s.io/api/apps/v1"
    33  	v1 "k8s.io/api/core/v1"
    34  	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    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/runtime"
    39  	"k8s.io/apimachinery/pkg/types"
    40  	"k8s.io/apimachinery/pkg/util/wait"
    41  )
    42  
    43  // yamlToUnstructured decodes a yaml file into an unstructured object
    44  func yamlToUnstructured(yamlPath string) (*unstructured.Unstructured, error) {
    45  	yamlFile, err := ioutil.ReadFile(yamlPath)
    46  	if err != nil {
    47  		return nil, fmt.Errorf("failed to read file %s: %v", yamlPath, err)
    48  	}
    49  	if bytes.Contains(yamlFile, []byte("\n---\n")) {
    50  		return nil, fmt.Errorf("custom resource manifest cannot have more than 1 resource")
    51  	}
    52  	obj := &unstructured.Unstructured{}
    53  	jsonSpec, err := yaml.YAMLToJSON(yamlFile)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("could not convert yaml file to json: %v", err)
    56  	}
    57  	if err := obj.UnmarshalJSON(jsonSpec); err != nil {
    58  		return nil, fmt.Errorf("failed to unmarshal custom resource manifest to unstructured: %s", err)
    59  	}
    60  	// set the namespace
    61  	obj.SetNamespace(viper.GetString(NamespaceOpt))
    62  	return obj, nil
    63  }
    64  
    65  // createFromYAMLFile will take a path to a YAML file and create the resource. If it finds a
    66  // deployment, it will add the scorecard proxy as a container in the deployments podspec.
    67  func createFromYAMLFile(yamlPath string) error {
    68  	yamlFile, err := ioutil.ReadFile(yamlPath)
    69  	if err != nil {
    70  		return fmt.Errorf("failed to read file %s: %v", yamlPath, err)
    71  	}
    72  	yamlSplit := bytes.Split(yamlFile, []byte("\n---\n"))
    73  	for _, yamlSpec := range yamlSplit {
    74  		// some autogenerated files may include an extra `---` at the end of the file
    75  		if string(yamlSpec) == "" {
    76  			continue
    77  		}
    78  		obj := &unstructured.Unstructured{}
    79  		jsonSpec, err := yaml.YAMLToJSON(yamlSpec)
    80  		if err != nil {
    81  			return fmt.Errorf("could not convert yaml file to json: %v", err)
    82  		}
    83  		if err := obj.UnmarshalJSON(jsonSpec); err != nil {
    84  			return fmt.Errorf("could not unmarshal resource spec: %v", err)
    85  		}
    86  		obj.SetNamespace(viper.GetString(NamespaceOpt))
    87  
    88  		// dirty hack to merge scorecard proxy into operator deployment; lots of serialization and deserialization
    89  		if obj.GetKind() == "Deployment" {
    90  			// TODO: support multiple deployments
    91  			if deploymentName != "" {
    92  				return fmt.Errorf("scorecard currently does not support multiple deployments in the manifests")
    93  			}
    94  			dep, err := unstructuredToDeployment(obj)
    95  			if err != nil {
    96  				return fmt.Errorf("failed to convert object to deployment: %v", err)
    97  			}
    98  			deploymentName = dep.GetName()
    99  			err = createKubeconfigSecret()
   100  			if err != nil {
   101  				return fmt.Errorf("failed to create kubeconfig secret for scorecard-proxy: %v", err)
   102  			}
   103  			addMountKubeconfigSecret(dep)
   104  			addProxyContainer(dep)
   105  			// go back to unstructured to create
   106  			obj, err = deploymentToUnstructured(dep)
   107  			if err != nil {
   108  				return fmt.Errorf("failed to convert deployment to unstructured: %v", err)
   109  			}
   110  		}
   111  		err = runtimeClient.Create(context.TODO(), obj)
   112  		if err != nil {
   113  			_, restErr := restMapper.RESTMappings(obj.GetObjectKind().GroupVersionKind().GroupKind())
   114  			if restErr == nil {
   115  				return err
   116  			}
   117  			// don't store error, as only error will be timeout. Error from runtime client will be easier for
   118  			// the user to understand than the timeout error, so just use that if we fail
   119  			_ = wait.PollImmediate(time.Second*1, time.Second*10, func() (bool, error) {
   120  				restMapper.Reset()
   121  				_, err := restMapper.RESTMappings(obj.GetObjectKind().GroupVersionKind().GroupKind())
   122  				if err != nil {
   123  					return false, nil
   124  				}
   125  				return true, nil
   126  			})
   127  			err = runtimeClient.Create(context.TODO(), obj)
   128  			if err != nil {
   129  				return err
   130  			}
   131  		}
   132  		addResourceCleanup(obj, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()})
   133  	}
   134  	return nil
   135  }
   136  
   137  // createKubeconfigSecret creates the secret that will be mounted in the operator's container and contains
   138  // the kubeconfig for communicating with the proxy
   139  func createKubeconfigSecret() error {
   140  	kubeconfigMap := make(map[string][]byte)
   141  	kc, err := proxyConf.Create(metav1.OwnerReference{Name: "scorecard"}, "http://localhost:8889", viper.GetString(NamespaceOpt))
   142  	if err != nil {
   143  		return err
   144  	}
   145  	defer func() {
   146  		if err := os.Remove(kc.Name()); err != nil {
   147  			log.Errorf("Failed to delete generated kubeconfig file: (%v)", err)
   148  		}
   149  	}()
   150  	kc, err = os.Open(kc.Name())
   151  	if err != nil {
   152  		return err
   153  	}
   154  	kcBytes, err := ioutil.ReadAll(kc)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	kubeconfigMap["kubeconfig"] = kcBytes
   159  	kubeconfigSecret := &v1.Secret{
   160  		ObjectMeta: metav1.ObjectMeta{
   161  			Name:      "scorecard-kubeconfig",
   162  			Namespace: viper.GetString(NamespaceOpt),
   163  		},
   164  		Data: kubeconfigMap,
   165  	}
   166  	err = runtimeClient.Create(context.TODO(), kubeconfigSecret)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	addResourceCleanup(kubeconfigSecret, types.NamespacedName{Namespace: kubeconfigSecret.GetNamespace(), Name: kubeconfigSecret.GetName()})
   171  	return nil
   172  }
   173  
   174  // addMountKubeconfigSecret creates the volume mount for the kubeconfig secret
   175  func addMountKubeconfigSecret(dep *appsv1.Deployment) {
   176  	// create mount for secret
   177  	dep.Spec.Template.Spec.Volumes = append(dep.Spec.Template.Spec.Volumes, v1.Volume{
   178  		Name: "scorecard-kubeconfig",
   179  		VolumeSource: v1.VolumeSource{Secret: &v1.SecretVolumeSource{
   180  			SecretName: "scorecard-kubeconfig",
   181  			Items: []v1.KeyToPath{{
   182  				Key:  "kubeconfig",
   183  				Path: "config",
   184  			}},
   185  		},
   186  		},
   187  	})
   188  	for index := range dep.Spec.Template.Spec.Containers {
   189  		// mount the volume
   190  		dep.Spec.Template.Spec.Containers[index].VolumeMounts = append(dep.Spec.Template.Spec.Containers[index].VolumeMounts, v1.VolumeMount{
   191  			Name:      "scorecard-kubeconfig",
   192  			MountPath: "/scorecard-secret",
   193  		})
   194  		// specify the path via KUBECONFIG env var
   195  		dep.Spec.Template.Spec.Containers[index].Env = append(dep.Spec.Template.Spec.Containers[index].Env, v1.EnvVar{
   196  			Name:  "KUBECONFIG",
   197  			Value: "/scorecard-secret/config",
   198  		})
   199  	}
   200  }
   201  
   202  // addProxyContainer adds the container spec for the scorecard-proxy to the deployment's podspec
   203  func addProxyContainer(dep *appsv1.Deployment) {
   204  	pullPolicyString := viper.GetString(ProxyPullPolicyOpt)
   205  	var pullPolicy v1.PullPolicy
   206  	switch pullPolicyString {
   207  	case "Always":
   208  		pullPolicy = v1.PullAlways
   209  	case "Never":
   210  		pullPolicy = v1.PullNever
   211  	case "PullIfNotPresent":
   212  		pullPolicy = v1.PullIfNotPresent
   213  	default:
   214  		// 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
   215  		pullPolicy = v1.PullAlways
   216  	}
   217  	dep.Spec.Template.Spec.Containers = append(dep.Spec.Template.Spec.Containers, v1.Container{
   218  		Name:            "scorecard-proxy",
   219  		Image:           viper.GetString(ProxyImageOpt),
   220  		ImagePullPolicy: pullPolicy,
   221  		Command:         []string{"scorecard-proxy"},
   222  		Env: []v1.EnvVar{{
   223  			Name:      k8sutil.WatchNamespaceEnvVar,
   224  			ValueFrom: &v1.EnvVarSource{FieldRef: &v1.ObjectFieldSelector{FieldPath: "metadata.namespace"}},
   225  		}},
   226  	})
   227  }
   228  
   229  // unstructuredToCRD converts an unstructured object to a CRD
   230  func unstructuredToCRD(obj *unstructured.Unstructured) (*apiextv1beta1.CustomResourceDefinition, error) {
   231  	jsonByte, err := obj.MarshalJSON()
   232  	if err != nil {
   233  		return nil, fmt.Errorf("failed to convert CRD to json: %v", err)
   234  	}
   235  	crdObj, _, err := dynamicDecoder.Decode(jsonByte, nil, nil)
   236  	if err != nil {
   237  		return nil, fmt.Errorf("failed to decode CRD object: %v", err)
   238  	}
   239  	switch o := crdObj.(type) {
   240  	case *apiextv1beta1.CustomResourceDefinition:
   241  		return o, nil
   242  	default:
   243  		return nil, fmt.Errorf("conversion of runtime object to CRD failed (resulting runtime object not CRD type)")
   244  	}
   245  }
   246  
   247  // unstructuredToDeployment converts an unstructured object to a deployment
   248  func unstructuredToDeployment(obj *unstructured.Unstructured) (*appsv1.Deployment, error) {
   249  	jsonByte, err := obj.MarshalJSON()
   250  	if err != nil {
   251  		return nil, fmt.Errorf("failed to convert deployment to json: %v", err)
   252  	}
   253  	depObj, _, err := dynamicDecoder.Decode(jsonByte, nil, nil)
   254  	if err != nil {
   255  		return nil, fmt.Errorf("failed to decode deployment object: %v", err)
   256  	}
   257  	switch o := depObj.(type) {
   258  	case *appsv1.Deployment:
   259  		return o, nil
   260  	default:
   261  		return nil, fmt.Errorf("conversion of runtime object to deployment failed (resulting runtime object not deployment type)")
   262  	}
   263  }
   264  
   265  // deploymentToUnstructured converts a deployment to an unstructured object
   266  func deploymentToUnstructured(dep *appsv1.Deployment) (*unstructured.Unstructured, error) {
   267  	jsonByte, err := json.Marshal(dep)
   268  	if err != nil {
   269  		return nil, fmt.Errorf("failed to remarshal deployment: %v", err)
   270  	}
   271  	obj := &unstructured.Unstructured{}
   272  	err = obj.UnmarshalJSON(jsonByte)
   273  	if err != nil {
   274  		return nil, fmt.Errorf("failed to unmarshal updated deployment: %v", err)
   275  	}
   276  	return obj, nil
   277  }
   278  
   279  // cleanupScorecard runs all cleanup functions in reverse order
   280  func cleanupScorecard() error {
   281  	failed := false
   282  	for i := len(cleanupFns) - 1; i >= 0; i-- {
   283  		err := cleanupFns[i]()
   284  		if err != nil {
   285  			failed = true
   286  			log.Printf("a cleanup function failed with error: %v\n", err)
   287  		}
   288  	}
   289  	if failed {
   290  		return fmt.Errorf("a cleanup function failed; see stdout for more details")
   291  	}
   292  	return nil
   293  }
   294  
   295  // addResourceCleanup adds a cleanup function for the specified runtime object
   296  func addResourceCleanup(obj runtime.Object, key types.NamespacedName) {
   297  	cleanupFns = append(cleanupFns, func() error {
   298  		// make a copy of the object because the client changes it
   299  		objCopy := obj.DeepCopyObject()
   300  		err := runtimeClient.Delete(context.TODO(), obj)
   301  		if err != nil {
   302  			return err
   303  		}
   304  		err = wait.PollImmediate(time.Second*1, time.Second*10, func() (bool, error) {
   305  			err = runtimeClient.Get(context.TODO(), key, objCopy)
   306  			if err != nil {
   307  				if apierrors.IsNotFound(err) {
   308  					return true, nil
   309  				}
   310  				return false, fmt.Errorf("error encountered during deletion of resource type %v with namespace/name (%+v): %v", objCopy.GetObjectKind().GroupVersionKind().Kind, key, err)
   311  			}
   312  			return false, nil
   313  		})
   314  		if err != nil {
   315  			return fmt.Errorf("cleanup function failed: %v", err)
   316  		}
   317  		return nil
   318  	})
   319  }