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 }