1// Copyright 2018 Istio 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 15package validate 16 17import ( 18 "encoding/json" 19 "errors" 20 "fmt" 21 "io" 22 "os" 23 "strings" 24 25 "github.com/gogo/protobuf/proto" 26 "github.com/hashicorp/go-multierror" 27 "github.com/spf13/cobra" 28 "gopkg.in/yaml.v2" 29 30 "istio.io/pkg/log" 31 32 mixercrd "istio.io/istio/mixer/pkg/config/crd" 33 mixerstore "istio.io/istio/mixer/pkg/config/store" 34 "istio.io/istio/mixer/pkg/runtime/config/constant" 35 mixervalidate "istio.io/istio/mixer/pkg/validate" 36 "istio.io/istio/pilot/pkg/model" 37 "istio.io/istio/pilot/pkg/serviceregistry/kube/controller" 38 "istio.io/istio/pkg/config/protocol" 39 "istio.io/istio/pkg/config/schema/collection" 40 "istio.io/istio/pkg/config/schema/collections" 41 "istio.io/istio/pkg/config/schema/resource" 42 "istio.io/istio/pkg/util/gogoprotomarshal" 43 44 operator_istio "istio.io/istio/operator/pkg/apis/istio" 45 operator_validate "istio.io/istio/operator/pkg/validate" 46 47 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 48 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 49 "k8s.io/apimachinery/pkg/runtime" 50) 51 52var ( 53 mixerAPIVersion = "config.istio.io/v1alpha2" 54 55 errMissingFilename = errors.New(`error: you must specify resources by --filename. 56Example resource specifications include: 57 '-f rsrc.yaml' 58 '--filename=rsrc.json'`) 59 errKindNotSupported = errors.New("kind is not supported") 60 61 validFields = map[string]struct{}{ 62 "apiVersion": {}, 63 "kind": {}, 64 "metadata": {}, 65 "spec": {}, 66 "status": {}, 67 } 68 69 validMixerKinds = map[string]struct{}{ 70 constant.RulesKind: {}, 71 constant.AdapterKind: {}, 72 constant.TemplateKind: {}, 73 constant.HandlerKind: {}, 74 constant.InstanceKind: {}, 75 constant.AttributeManifestKind: {}, 76 } 77 78 istioDeploymentLabel = []string{ 79 "app", 80 "version", 81 } 82 serviceProtocolUDP = "UDP" 83) 84 85type validator struct { 86 mixerValidator mixerstore.BackendValidator 87} 88 89func checkFields(un *unstructured.Unstructured) error { 90 var errs error 91 for key := range un.Object { 92 if _, ok := validFields[key]; !ok { 93 errs = multierror.Append(errs, fmt.Errorf("unknown field %q", key)) 94 } 95 } 96 return errs 97} 98 99func (v *validator) validateResource(istioNamespace string, un *unstructured.Unstructured) error { 100 gvk := resource.GroupVersionKind{ 101 Group: un.GroupVersionKind().Group, 102 Version: un.GroupVersionKind().Version, 103 Kind: un.GroupVersionKind().Kind, 104 } 105 // TODO(jasonwzm) remove this when multi-version is supported. v1beta1 shares the same 106 // schema as v1lalpha3. Fake conversion and validate against v1alpha3. 107 if gvk.Group == "networking.istio.io" && gvk.Version == "v1beta1" { 108 gvk.Version = "v1alpha3" 109 } 110 schema, exists := collections.Pilot.FindByGroupVersionKind(gvk) 111 if exists { 112 obj, err := convertObjectFromUnstructured(schema, un, "") 113 if err != nil { 114 return fmt.Errorf("cannot parse proto message: %v", err) 115 } 116 if err = checkFields(un); err != nil { 117 return err 118 } 119 return schema.Resource().ValidateProto(obj.Name, obj.Namespace, obj.Spec) 120 } 121 122 if v.mixerValidator != nil && un.GetAPIVersion() == mixerAPIVersion { 123 if !v.mixerValidator.SupportsKind(un.GetKind()) { 124 return errKindNotSupported 125 } 126 if err := checkFields(un); err != nil { 127 return err 128 } 129 if _, ok := validMixerKinds[un.GetKind()]; !ok { 130 log.Warnf("deprecated Mixer kind %q, please use %q or %q instead", un.GetKind(), 131 constant.HandlerKind, constant.InstanceKind) 132 } 133 134 return v.mixerValidator.Validate(&mixerstore.BackendEvent{ 135 Type: mixerstore.Update, 136 Key: mixerstore.Key{ 137 Name: un.GetName(), 138 Namespace: un.GetNamespace(), 139 Kind: un.GetKind(), 140 }, 141 Value: mixercrd.ToBackEndResource(un), 142 }) 143 } 144 var errs error 145 if un.IsList() { 146 _ = un.EachListItem(func(item runtime.Object) error { 147 castItem := item.(*unstructured.Unstructured) 148 if castItem.GetKind() == "Service" { 149 err := v.validateServicePortPrefix(istioNamespace, castItem) 150 if err != nil { 151 errs = multierror.Append(errs, err) 152 } 153 } 154 if castItem.GetKind() == "Deployment" { 155 v.validateDeploymentLabel(istioNamespace, castItem) 156 } 157 return nil 158 }) 159 } 160 161 if errs != nil { 162 return errs 163 } 164 if un.GetKind() == "Service" { 165 return v.validateServicePortPrefix(istioNamespace, un) 166 } 167 168 if un.GetKind() == "Deployment" { 169 v.validateDeploymentLabel(istioNamespace, un) 170 return nil 171 } 172 173 if un.GetAPIVersion() == "install.istio.io/v1alpha1" { 174 if un.GetKind() == "IstioOperator" { 175 if err := checkFields(un); err != nil { 176 return err 177 } 178 179 // IstioOperator isn't part of pkg/config/schema/collections, 180 // usual conversion not available. Convert unstructured to string 181 // and ask operator code to check. 182 183 un.SetCreationTimestamp(metav1.Time{}) // UnmarshalIstioOperator chokes on these 184 by, err := json.Marshal(un) 185 if err != nil { 186 return err 187 } 188 189 iop, err := operator_istio.UnmarshalIstioOperator(string(by)) 190 if err != nil { 191 return err 192 } 193 194 return operator_validate.CheckIstioOperator(iop, true) 195 } 196 } 197 198 // Didn't really validate. This is OK, as we often get non-Istio Kubernetes YAML 199 // we can't complain about. 200 201 return nil 202} 203 204func (v *validator) validateServicePortPrefix(istioNamespace string, un *unstructured.Unstructured) error { 205 var errs error 206 if un.GetNamespace() == handleNamespace(istioNamespace) { 207 return nil 208 } 209 spec := un.Object["spec"].(map[string]interface{}) 210 if _, ok := spec["ports"]; ok { 211 ports := spec["ports"].([]interface{}) 212 for _, port := range ports { 213 p := port.(map[string]interface{}) 214 if p["protocol"] != nil && strings.EqualFold(p["protocol"].(string), serviceProtocolUDP) { 215 continue 216 } 217 if p["name"] == nil { 218 errs = multierror.Append(errs, fmt.Errorf("service %q has an unnamed port. This is not recommended,"+ 219 " see https://istio.io/docs/setup/kubernetes/prepare/requirements/", fmt.Sprintf("%s/%s/:", 220 un.GetName(), un.GetNamespace()))) 221 continue 222 } 223 if servicePortPrefixed(p["name"].(string)) { 224 errs = multierror.Append(errs, fmt.Errorf("service %q port %q does not follow the Istio naming convention."+ 225 " See https://istio.io/docs/setup/kubernetes/prepare/requirements/", fmt.Sprintf("%s/%s/:", 226 un.GetName(), un.GetNamespace()), p["name"].(string))) 227 } 228 } 229 } 230 if errs != nil { 231 return errs 232 } 233 return nil 234} 235 236func (v *validator) validateDeploymentLabel(istioNamespace string, un *unstructured.Unstructured) { 237 if un.GetNamespace() == handleNamespace(istioNamespace) { 238 return 239 } 240 labels := un.GetLabels() 241 for _, l := range istioDeploymentLabel { 242 if _, ok := labels[l]; !ok { 243 log.Warnf("deployment %q may not provide Istio metrics and telemetry without label %q."+ 244 " See https://istio.io/docs/setup/kubernetes/prepare/requirements/ \n", fmt.Sprintf("%s/%s:", 245 un.GetName(), un.GetNamespace()), l) 246 } 247 } 248} 249 250func (v *validator) validateFile(istioNamespace *string, reader io.Reader) error { 251 decoder := yaml.NewDecoder(reader) 252 var errs error 253 for { 254 // YAML allows non-string keys and the produces generic keys for nested fields 255 raw := make(map[interface{}]interface{}) 256 err := decoder.Decode(&raw) 257 if err == io.EOF { 258 return errs 259 } 260 if err != nil { 261 errs = multierror.Append(errs, err) 262 return errs 263 } 264 if len(raw) == 0 { 265 continue 266 } 267 out := transformInterfaceMap(raw) 268 un := unstructured.Unstructured{Object: out} 269 err = v.validateResource(*istioNamespace, &un) 270 if err != nil { 271 errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("%s/%s/%s:", 272 un.GetKind(), un.GetNamespace(), un.GetName()))) 273 } 274 } 275} 276 277func validateFiles(istioNamespace *string, filenames []string, referential bool, writer io.Writer) error { 278 if len(filenames) == 0 { 279 return errMissingFilename 280 } 281 282 v := &validator{ 283 mixerValidator: mixervalidate.NewDefaultValidator(referential), 284 } 285 286 var errs, err error 287 var reader io.Reader 288 for _, filename := range filenames { 289 if filename == "-" { 290 reader = os.Stdin 291 } else { 292 reader, err = os.Open(filename) 293 } 294 if err != nil { 295 errs = multierror.Append(errs, fmt.Errorf("cannot read file %q: %v", filename, err)) 296 continue 297 } 298 err = v.validateFile(istioNamespace, reader) 299 if err != nil { 300 errs = multierror.Append(errs, err) 301 } 302 } 303 if errs != nil { 304 return errs 305 } 306 for _, fname := range filenames { 307 if fname == "-" { 308 _, _ = fmt.Fprintf(writer, "validation succeed\n") 309 break 310 } else { 311 _, _ = fmt.Fprintf(writer, "%q is valid\n", fname) 312 } 313 } 314 315 return nil 316} 317 318// NewValidateCommand creates a new command for validating Istio k8s resources. 319func NewValidateCommand(istioNamespace *string) *cobra.Command { 320 var filenames []string 321 var referential bool 322 323 c := &cobra.Command{ 324 Use: "validate -f FILENAME [options]", 325 Short: "Validate Istio policy and rules (NOTE: validate is deprecated and will be removed in 1.6. Use 'istioctl analyze' to validate configuration.)", 326 Example: ` 327 # Validate bookinfo-gateway.yaml 328 istioctl validate -f bookinfo-gateway.yaml 329 330 # Validate current deployments under 'default' namespace within the cluster 331 kubectl get deployments -o yaml |istioctl validate -f - 332 333 # Validate current services under 'default' namespace within the cluster 334 kubectl get services -o yaml |istioctl validate -f - 335 336 # Also see the related command 'istioctl analyze' 337 istioctl analyze samples/bookinfo/networking/bookinfo-gateway.yaml 338`, 339 Args: cobra.NoArgs, 340 RunE: func(c *cobra.Command, _ []string) error { 341 return validateFiles(istioNamespace, filenames, referential, c.OutOrStderr()) 342 }, 343 } 344 345 flags := c.PersistentFlags() 346 flags.StringSliceVarP(&filenames, "filename", "f", nil, "Names of files to validate") 347 flags.BoolVarP(&referential, "referential", "x", true, "Enable structural validation for policy and telemetry") 348 349 return c 350} 351 352func transformInterfaceArray(in []interface{}) []interface{} { 353 out := make([]interface{}, len(in)) 354 for i, v := range in { 355 out[i] = transformMapValue(v) 356 } 357 return out 358} 359 360func transformInterfaceMap(in map[interface{}]interface{}) map[string]interface{} { 361 out := make(map[string]interface{}, len(in)) 362 for k, v := range in { 363 out[fmt.Sprintf("%v", k)] = transformMapValue(v) 364 } 365 return out 366} 367 368func transformMapValue(in interface{}) interface{} { 369 switch v := in.(type) { 370 case []interface{}: 371 return transformInterfaceArray(v) 372 case map[interface{}]interface{}: 373 return transformInterfaceMap(v) 374 default: 375 return v 376 } 377} 378 379func servicePortPrefixed(n string) bool { 380 i := strings.IndexByte(n, '-') 381 if i >= 0 { 382 n = n[:i] 383 } 384 p := protocol.Parse(n) 385 return p == protocol.Unsupported 386} 387func handleNamespace(istioNamespace string) string { 388 if istioNamespace == "" { 389 istioNamespace = controller.IstioNamespace 390 } 391 return istioNamespace 392} 393 394// TODO(nmittler): Remove this once Pilot migrates to galley schema. 395func convertObjectFromUnstructured(schema collection.Schema, un *unstructured.Unstructured, domain string) (*model.Config, error) { 396 data, err := fromSchemaAndJSONMap(schema, un.Object["spec"]) 397 if err != nil { 398 return nil, err 399 } 400 401 return &model.Config{ 402 ConfigMeta: model.ConfigMeta{ 403 Type: schema.Resource().Kind(), 404 Group: schema.Resource().Group(), 405 Version: schema.Resource().Version(), 406 Name: un.GetName(), 407 Namespace: un.GetNamespace(), 408 Domain: domain, 409 Labels: un.GetLabels(), 410 Annotations: un.GetAnnotations(), 411 ResourceVersion: un.GetResourceVersion(), 412 CreationTimestamp: un.GetCreationTimestamp().Time, 413 }, 414 Spec: data, 415 }, nil 416} 417 418// TODO(nmittler): Remove this once Pilot migrates to galley schema. 419func fromSchemaAndYAML(schema collection.Schema, yml string) (proto.Message, error) { 420 pb, err := schema.Resource().NewProtoInstance() 421 if err != nil { 422 return nil, err 423 } 424 if err = gogoprotomarshal.ApplyYAMLStrict(yml, pb); err != nil { 425 return nil, err 426 } 427 return pb, nil 428} 429 430// TODO(nmittler): Remove this once Pilot migrates to galley schema. 431func fromSchemaAndJSONMap(schema collection.Schema, data interface{}) (proto.Message, error) { 432 // Marshal to YAML bytes 433 str, err := yaml.Marshal(data) 434 if err != nil { 435 return nil, err 436 } 437 out, err := fromSchemaAndYAML(schema, string(str)) 438 if err != nil { 439 return nil, multierror.Prefix(err, fmt.Sprintf("YAML decoding error: %v", string(str))) 440 } 441 return out, nil 442} 443