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 }