// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "github.com/spf13/cobra" "istio.io/istio/istioctl/pkg/kubernetes" "istio.io/istio/istioctl/pkg/writer/pilot" ) func tlsCheck() *cobra.Command { cmd := &cobra.Command{ Use: "tls-check <pod-name[.namespace]> [<service>]", Short: "Check whether TLS setting are matching between authentication policy and destination rules", Long: ` Check what authentication policies and destination rules pilot uses to config a proxy instance, and check if TLS settings are compatible between them. `, Example: ` # Check settings for pod "foo-656bd7df7c-5zp4s" in namespace default: istioctl authn tls-check 656bd7df7c-5zp4s.default # Check settings for pod "foo-656bd7df7c-5zp4s" in namespace default, filtered on destintation service "bar" : istioctl authn tls-check 656bd7df7c-5zp4s.default bar `, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { kubeClient, err := kubernetes.NewClient(kubeconfig, configContext) if err != nil { return err } podName, ns := inferPodInfo(args[0], handleNamespace()) debug, err := kubeClient.PilotDiscoveryDo(istioNamespace, "GET", fmt.Sprintf("/debug/authenticationz?proxyID=%s.%s", podName, ns), nil) if err != nil { return err } tcw := pilot.TLSCheckWriter{Writer: cmd.OutOrStdout()} if len(args) >= 2 { return tcw.PrintSingle(debug, args[1]) } return tcw.PrintAll(debug) }, } return cmd } // AuthN provides a command named authn that allows user to interact with Istio authentication policies. func AuthN() *cobra.Command { cmd := &cobra.Command{ Use: "authn", Short: "Interact with Istio authentication policies", Long: ` A group of commands used to interact with Istio authentication policies. tls-check `, Example: `# Check whether TLS setting are matching between authentication policy and destination rules: istioctl authn tls-check`, } cmd.AddCommand(tlsCheck()) return cmd } func init() { rootCmd.AddCommand(AuthN()) }
// Copyright 2017 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "encoding/json" "fmt" "io" "io/ioutil" "os" "github.com/ghodss/yaml" multierror "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "k8s.io/api/extensions/v1beta1" "istio.io/istio/istioctl/pkg/convert" "istio.io/istio/pilot/pkg/config/kube/crd" "istio.io/istio/pilot/pkg/model" "istio.io/istio/pkg/log" ) var ( inFilenames []string outConvertFilename string convertIngressCmd = &cobra.Command{ Use: "convert-ingress", Short: "Convert Ingress configuration into Istio VirtualService configuration", Long: "Converts Ingresses into VirtualService configuration on a best effort basis. " + "The output should be considered a starting point for your Istio configuration and probably " + "require some minor modification. " + "Warnings will be generated where configs cannot be converted perfectly. " + "The input must be a Kubernetes Ingress. " + "The conversion of v1alpha1 Istio rules has been removed from istioctl.", Example: "istioctl experimental convert-ingress -f samples/bookinfo/platform/kube/bookinfo-ingress.yaml", RunE: func(c *cobra.Command, args []string) error { if len(inFilenames) == 0 { return fmt.Errorf("no input files provided") } readers := make([]io.Reader, 0) if len(inFilenames) == 1 && inFilenames[0] == "-" { readers = append(readers, os.Stdin) } else { for _, filename := range inFilenames { file, err := os.Open(filename) if err != nil { return err } defer func() { if err := file.Close(); err != nil { log.Errorf("Did not close input %s successfully: %v", filename, err) } }() readers = append(readers, file) } } writer := os.Stdout if outConvertFilename != "-" { file, err := os.Create(outConvertFilename) if err != nil { return err } defer func() { if err := file.Close(); err != nil { log.Errorf("Did not close output successfully: %v", err) } }() writer = file } return convertConfigs(readers, writer) }, } ) func convertConfigs(readers []io.Reader, writer io.Writer) error { configDescriptor := model.ConfigDescriptor{ model.VirtualService, model.Gateway, } configs, ingresses, err := readConfigs(readers) if err != nil { return err } if err = validateConfigs(configs); err != nil { return err } // ingresses without specified namespace need to generate valid output; use the default for _, ingress := range ingresses { if ingress.Namespace == "" { ingress.Namespace = defaultNamespace } } out := make([]model.Config, 0) convertedIngresses, err := convert.IstioIngresses(ingresses, "") if err == nil { out = append(out, convertedIngresses...) } else { return multierror.Prefix(err, "Ingress rules invalid") } writeYAMLOutput(configDescriptor, out, writer) // sanity check that the outputs are valid if err := validateConfigs(out); err != nil { return multierror.Prefix(err, "output config(s) are invalid:") } return nil } func readConfigs(readers []io.Reader) ([]model.Config, []*v1beta1.Ingress, error) { out := make([]model.Config, 0) outIngresses := make([]*v1beta1.Ingress, 0) for _, reader := range readers { data, err := ioutil.ReadAll(reader) if err != nil { return nil, nil, err } configs, kinds, err := crd.ParseInputs(string(data)) if err != nil { return nil, nil, err } recognized := 0 for _, nonIstio := range kinds { if nonIstio.Kind == "Ingress" && nonIstio.APIVersion == "extensions/v1beta1" { ingress, err := parseIngress(nonIstio) if err != nil { log.Errorf("Could not decode ingress %v: %v", nonIstio.Name, err) continue } outIngresses = append(outIngresses, ingress) recognized++ } } if len(kinds) > recognized { // If convert-networking-config was asked to convert non-network things, // like Deployments and Services, return a brief informative error kindsFound := make(map[string]bool) for _, kind := range kinds { kindsFound[kind.Kind] = true } var msg error for kind := range kindsFound { msg = multierror.Append(msg, fmt.Errorf("unsupported kind: %v", kind)) } return nil, nil, msg } out = append(out, configs...) } return out, outIngresses, nil } func writeYAMLOutput(descriptor model.ConfigDescriptor, configs []model.Config, writer io.Writer) { for i, config := range configs { schema, exists := descriptor.GetByType(config.Type) if !exists { log.Errorf("Unknown kind %q for %v", crd.ResourceName(config.Type), config.Name) continue } obj, err := crd.ConvertConfig(schema, config) if err != nil { log.Errorf("Could not decode %v: %v", config.Name, err) continue } bytes, err := yaml.Marshal(obj) if err != nil { log.Errorf("Could not convert %v to YAML: %v", config, err) continue } writer.Write(bytes) // nolint: errcheck if i+1 < len(configs) { writer.Write([]byte("---\n")) // nolint: errcheck } } } func validateConfigs(configs []model.Config) error { var errs error for _, config := range configs { var err error switch config.Type { case model.VirtualService.Type: err = model.ValidateVirtualService(config.Name, config.Namespace, config.Spec) } if err != nil { errs = multierror.Append(err, errs) } } return errs } func parseIngress(unparsed crd.IstioKind) (*v1beta1.Ingress, error) { // To convert unparsed to a v1beta1.Ingress Marshal into JSON and Unmarshal back b, err := json.Marshal(unparsed) if err != nil { return nil, multierror.Prefix(err, "can't reserialize Ingress") } out := &v1beta1.Ingress{} err = json.Unmarshal(b, out) if err != nil { return nil, multierror.Prefix(err, "can't deserialize as Ingress") } return out, nil } func init() { convertIngressCmd.PersistentFlags().StringSliceVarP(&inFilenames, "filenames", "f", nil, "Input filenames") convertIngressCmd.PersistentFlags().StringVarP(&outConvertFilename, "output", "o", "-", "Output filename") experimentalCmd.AddCommand(convertIngressCmd) }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // DEPRECATED - These commands are deprecated and will be removed in future releases. package main import ( "errors" "fmt" "io" "io/ioutil" "net/url" "os" "sort" "strings" "text/tabwriter" "time" "github.com/ghodss/yaml" multierror "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "istio.io/api/networking/v1alpha3" "istio.io/istio/pilot/pkg/config/kube/crd" "istio.io/istio/pilot/pkg/model" kubecfg "istio.io/istio/pkg/kube" "istio.io/istio/pkg/log" ) const ( // Headings for short format listing of unknown types unknownShortOutputHeading = "NAME\tKIND\tNAMESPACE\tAGE" ) var ( istioContext string istioAPIServer string getAllNamespaces bool // Create a model.ConfigStore (or sortedConfigStore) clientFactory = newClient // sortWeight defines the output order for "get all". We show the V3 types first. sortWeight = map[string]int{ model.Gateway.Type: 10, model.VirtualService.Type: 5, model.DestinationRule.Type: 3, model.ServiceEntry.Type: 1, } // mustList tracks which Istio types we SHOULD NOT silently ignore if we can't list. // The user wants reasonable error messages when doing `get all` against a different // server version. mustList = map[string]bool{ model.Gateway.Type: true, model.VirtualService.Type: true, model.DestinationRule.Type: true, model.ServiceEntry.Type: true, model.HTTPAPISpec.Type: true, model.HTTPAPISpecBinding.Type: true, model.QuotaSpec.Type: true, model.QuotaSpecBinding.Type: true, model.AuthenticationPolicy.Type: true, model.ServiceRole.Type: true, model.ServiceRoleBinding.Type: true, model.RbacConfig.Type: true, } // Headings for short format listing specific to type shortOutputHeadings = map[string]string{ "gateway": "GATEWAY NAME\tHOSTS\tNAMESPACE\tAGE", "virtual-service": "VIRTUAL-SERVICE NAME\tGATEWAYS\tHOSTS\t#HTTP\t#TCP\tNAMESPACE\tAGE", "destination-rule": "DESTINATION-RULE NAME\tHOST\tSUBSETS\tNAMESPACE\tAGE", "service-entry": "SERVICE-ENTRY NAME\tHOSTS\tPORTS\tNAMESPACE\tAGE", } // Formatters for short format listing specific to type shortOutputters = map[string]func(model.Config, io.Writer){ "gateway": printShortGateway, "virtual-service": printShortVirtualService, "destination-rule": printShortDestinationRule, "service-entry": printShortServiceEntry, } // all resources will be migrated out of config.istio.io to their own api group mapping to package path. // TODO(xiaolanz) legacy group exists until we find out a client for mixer legacyIstioAPIGroupVersion = schema.GroupVersion{ Group: "config.istio.io", Version: "v1alpha2", } postCmd = &cobra.Command{ Use: "create", Deprecated: "Use `kubectl create` instead (see https://kubernetes.io/docs/tasks/tools/install-kubectl)", Short: "Create policies and rules", Example: "istioctl create -f example-routing.yaml", RunE: func(c *cobra.Command, args []string) error { if len(args) != 0 { c.Println(c.UsageString()) return fmt.Errorf("create takes no arguments") } varr, others, err := readInputs() if err != nil { return err } if len(varr) == 0 && len(others) == 0 { return errors.New("nothing to create") } for _, config := range varr { if config.Namespace, err = handleNamespaces(config.Namespace); err != nil { return err } var configClient model.ConfigStore if configClient, err = clientFactory(); err != nil { return err } var rev string if rev, err = configClient.Create(config); err != nil { return err } c.Printf("Created config %v at revision %v\n", config.Key(), rev) } if len(others) > 0 { if err = preprocMixerConfig(others); err != nil { return err } otherClient, resources, oerr := prepareClientForOthers(others) if oerr != nil { return oerr } var errs *multierror.Error var updated crd.IstioKind for _, config := range others { resource, ok := resources[config.Kind] if !ok { errs = multierror.Append(errs, fmt.Errorf("kind %s is not known", config.Kind)) continue } err = otherClient.Post(). Namespace(config.Namespace). Resource(resource.Name). Body(&config). Do(). Into(&updated) if err != nil { errs = multierror.Append(errs, err) continue } key := model.Key(config.Kind, config.Name, config.Namespace) fmt.Printf("Created config %s at revision %v\n", key, updated.ResourceVersion) } if errs != nil { return errs } } return nil }, } putCmd = &cobra.Command{ Use: "replace", Deprecated: "Use `kubectl apply` instead (see https://kubernetes.io/docs/tasks/tools/install-kubectl)", Short: "Replace existing policies and rules", Example: "istioctl replace -f example-routing.yaml", RunE: func(c *cobra.Command, args []string) error { if len(args) != 0 { c.Println(c.UsageString()) return fmt.Errorf("replace takes no arguments") } varr, others, err := readInputs() if err != nil { return err } if len(varr) == 0 && len(others) == 0 { return errors.New("nothing to replace") } for _, config := range varr { if config.Namespace, err = handleNamespaces(config.Namespace); err != nil { return err } var configClient model.ConfigStore if configClient, err = clientFactory(); err != nil { return err } // fill up revision if config.ResourceVersion == "" { current := configClient.Get(config.Type, config.Name, config.Namespace) if current != nil { config.ResourceVersion = current.ResourceVersion } } var newRev string if newRev, err = configClient.Update(config); err != nil { return err } fmt.Printf("Updated config %v to revision %v\n", config.Key(), newRev) } if len(others) > 0 { if err = preprocMixerConfig(others); err != nil { return err } otherClient, resources, oerr := prepareClientForOthers(others) if oerr != nil { return oerr } var errs *multierror.Error var current crd.IstioKind var updated crd.IstioKind for _, config := range others { resource, ok := resources[config.Kind] if !ok { errs = multierror.Append(errs, fmt.Errorf("kind %s is not known", config.Kind)) continue } if config.ResourceVersion == "" { err = otherClient.Get(). Namespace(config.Namespace). Name(config.Name). Resource(resource.Name). Do(). Into(¤t) if err == nil && current.ResourceVersion != "" { config.ResourceVersion = current.ResourceVersion } } err = otherClient.Put(). Namespace(config.Namespace). Name(config.Name). Resource(resource.Name). Body(&config). Do(). Into(&updated) if err != nil { errs = multierror.Append(errs, err) continue } key := model.Key(config.Kind, config.Name, config.Namespace) fmt.Printf("Updated config %s to revision %v\n", key, updated.ResourceVersion) } if errs != nil { return errs } } return nil }, } getCmd = &cobra.Command{ Use: "get <type> [<name>]", Deprecated: "Use `kubectl get` instead (see https://kubernetes.io/docs/tasks/tools/install-kubectl)", Short: "Retrieve policies and rules", Example: `# List all virtual services istioctl get virtualservices # List all destination rules istioctl get destinationrules # Get a specific virtual service named bookinfo istioctl get virtualservice bookinfo `, RunE: func(c *cobra.Command, args []string) error { configClient, err := clientFactory() if err != nil { return err } if len(args) < 1 { c.Println(c.UsageString()) return fmt.Errorf("specify the type of resource to get. Types are %v", strings.Join(supportedTypes(configClient), ", ")) } getByName := len(args) > 1 if getAllNamespaces && getByName { return errors.New("a resource cannot be retrieved by name across all namespaces") } var typs []model.ProtoSchema if !getByName && strings.ToLower(args[0]) == "all" { typs = configClient.ConfigDescriptor() } else { typ, err := protoSchema(configClient, args[0]) if err != nil { c.Println(c.UsageString()) return err } typs = []model.ProtoSchema{typ} } var ns string if getAllNamespaces { ns = v1.NamespaceAll } else { ns, _ = handleNamespaces(namespace) } var errs error var configs []model.Config if getByName { config := configClient.Get(typs[0].Type, args[1], ns) if config != nil { configs = append(configs, *config) } } else { for _, typ := range typs { typeConfigs, err := configClient.List(typ.Type, ns) if err == nil { configs = append(configs, typeConfigs...) } else { if mustList[typ.Type] { errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("Can't list %v:", typ.Type))) } } } } if len(configs) == 0 { c.Println("No resources found.") return errs } var outputters = map[string](func(io.Writer, model.ConfigStore, []model.Config)){ "yaml": printYamlOutput, "short": printShortOutput, } if outputFunc, ok := outputters[outputFormat]; ok { outputFunc(c.OutOrStdout(), configClient, configs) } else { return fmt.Errorf("unknown output format %v. Types are yaml|short", outputFormat) } return errs }, ValidArgs: configTypeResourceNames(model.IstioConfigTypes), ArgAliases: configTypePluralResourceNames(model.IstioConfigTypes), } deleteCmd = &cobra.Command{ Use: "delete <type> <name> [<name2> ... <nameN>]", Deprecated: "Use `kubectl delete` instead (see https://kubernetes.io/docs/tasks/tools/install-kubectl)", Short: "Delete policies or rules", Example: `# Delete a rule using the definition in example-routing.yaml. istioctl delete -f example-routing.yaml # Delete the virtual service bookinfo istioctl delete virtualservice bookinfo `, RunE: func(c *cobra.Command, args []string) error { configClient, errs := clientFactory() if errs != nil { return errs } // If we did not receive a file option, get names of resources to delete from command line if file == "" { if len(args) < 2 { c.Println(c.UsageString()) return fmt.Errorf("provide configuration type and name or -f option") } typ, err := protoSchema(configClient, args[0]) if err != nil { return err } ns, err := handleNamespaces(namespace) if err != nil { return err } for i := 1; i < len(args); i++ { if err := configClient.Delete(typ.Type, args[i], ns); err != nil { errs = multierror.Append(errs, fmt.Errorf("cannot delete %s: %v", args[i], err)) } else { c.Printf("Deleted config: %v %v\n", args[0], args[i]) } } return errs } // As we did get a file option, make sure the command line did not include any resources to delete if len(args) != 0 { c.Println(c.UsageString()) return fmt.Errorf("delete takes no arguments when the file option is used") } varr, others, err := readInputs() if err != nil { return err } if len(varr) == 0 && len(others) == 0 { return errors.New("nothing to delete") } for _, config := range varr { if config.Namespace, err = handleNamespaces(config.Namespace); err != nil { return err } // compute key if necessary if err = configClient.Delete(config.Type, config.Name, config.Namespace); err != nil { errs = multierror.Append(errs, fmt.Errorf("cannot delete %s: %v", config.Key(), err)) } else { c.Printf("Deleted config: %v\n", config.Key()) } } if errs != nil { return errs } if len(others) > 0 { if err = preprocMixerConfig(others); err != nil { return err } otherClient, resources, oerr := prepareClientForOthers(others) if oerr != nil { return oerr } for _, config := range others { resource, ok := resources[config.Kind] if !ok { errs = multierror.Append(errs, fmt.Errorf("kind %s is not known", config.Kind)) continue } err = otherClient.Delete(). Namespace(config.Namespace). Resource(resource.Name). Name(config.Name). Do(). Error() if err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to delete: %v", err)) continue } fmt.Printf("Deleted config: %s\n", model.Key(config.Kind, config.Name, config.Namespace)) } } return errs }, ValidArgs: configTypeResourceNames(model.IstioConfigTypes), ArgAliases: configTypePluralResourceNames(model.IstioConfigTypes), } contextCmd = &cobra.Command{ Use: "context-create --api-server http://<ip>:<port>", Deprecated: `Use kubectl instead (see https://kubernetes.io/docs/tasks/tools/install-kubectl), e.g. $ kubectl config set-context istio --cluster=istio $ kubectl config set-cluster istio --server=http://localhost:8080 $ kubectl config use-context istio `, Short: "Create a kubeconfig file suitable for use with istioctl in a non-Kubernetes environment", Example: `# Create a config file for the api server. istioctl context-create --api-server http://127.0.0.1:8080 `, RunE: func(c *cobra.Command, args []string) error { if istioAPIServer == "" { c.Println(c.UsageString()) return fmt.Errorf("specify the the Istio api server IP") } u, err := url.ParseRequestURI(istioAPIServer) if err != nil { c.Println(c.UsageString()) return err } configAccess := clientcmd.NewDefaultPathOptions() // use specified kubeconfig file for the location of the config to create or modify configAccess.GlobalFile = kubeconfig // gets existing kubeconfig or returns new empty config config, err := configAccess.GetStartingConfig() if err != nil { return err } cluster, exists := config.Clusters[istioContext] if !exists { cluster = clientcmdapi.NewCluster() } cluster.Server = u.String() config.Clusters[istioContext] = cluster context, exists := config.Contexts[istioContext] if !exists { context = clientcmdapi.NewContext() } context.Cluster = istioContext config.Contexts[istioContext] = context contextSwitched := false if config.CurrentContext != "" && config.CurrentContext != istioContext { contextSwitched = true } config.CurrentContext = istioContext if err = clientcmd.ModifyConfig(configAccess, *config, false); err != nil { return err } if contextSwitched { fmt.Printf("kubeconfig context switched to %q\n", istioContext) } fmt.Println("Context created") return nil }, } ) // The protoSchema is based on the kind (for example "virtualservice" or "destinationrule") func protoSchema(configClient model.ConfigStore, typ string) (model.ProtoSchema, error) { for _, desc := range configClient.ConfigDescriptor() { switch strings.ToLower(typ) { case crd.ResourceName(desc.Type), crd.ResourceName(desc.Plural): return desc, nil case desc.Type, desc.Plural: // legacy hyphenated resources names return model.ProtoSchema{}, fmt.Errorf("%q not recognized. Please use non-hyphenated resource name %q", typ, crd.ResourceName(typ)) } } return model.ProtoSchema{}, fmt.Errorf("configuration type %s not found, the types are %v", typ, strings.Join(supportedTypes(configClient), ", ")) } // readInputs reads multiple documents from the input and checks with the schema func readInputs() ([]model.Config, []crd.IstioKind, error) { var reader io.Reader switch file { case "": return nil, nil, errors.New("filename not specified (see --filename or -f)") case "-": reader = os.Stdin default: var err error var in *os.File if in, err = os.Open(file); err != nil { return nil, nil, err } defer func() { if err = in.Close(); err != nil { log.Errorf("Error: close file from %s, %s", file, err) } }() reader = in } input, err := ioutil.ReadAll(reader) if err != nil { return nil, nil, err } return crd.ParseInputsWithoutValidation(string(input)) } // Print a simple list of names func printShortOutput(writer io.Writer, _ model.ConfigStore, configList []model.Config) { // Sort configList by Type sort.Slice(configList, func(i, j int) bool { return sortWeight[configList[i].Type] < sortWeight[configList[j].Type] }) var w tabwriter.Writer w.Init(writer, 10, 4, 3, ' ', 0) prevType := "" var outputter func(model.Config, io.Writer) for _, c := range configList { if prevType != c.Type { if prevType != "" { // Place a newline between types when doing 'get all' fmt.Fprintf(&w, "\n") } heading, ok := shortOutputHeadings[c.Type] if !ok { heading = unknownShortOutputHeading } fmt.Fprintf(&w, "%s\n", heading) prevType = c.Type if outputter, ok = shortOutputters[c.Type]; !ok { outputter = printShortConfig } } outputter(c, &w) } w.Flush() // nolint: errcheck } func kindAsString(config model.Config) string { return fmt.Sprintf("%s.%s.%s", crd.KebabCaseToCamelCase(config.Type), config.Group, config.Version, ) } func printShortConfig(config model.Config, w io.Writer) { fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", config.Name, kindAsString(config), config.Namespace, renderTimestamp(config.CreationTimestamp)) } func printShortVirtualService(config model.Config, w io.Writer) { virtualService, ok := config.Spec.(*v1alpha3.VirtualService) if !ok { fmt.Fprintf(w, "Not a virtualservice: %v", config) return } fmt.Fprintf(w, "%s\t%s\t%s\t%5d\t%4d\t%s\t%s\n", config.Name, strings.Join(virtualService.Gateways, ","), strings.Join(virtualService.Hosts, ","), len(virtualService.Http), len(virtualService.Tcp), config.Namespace, renderTimestamp(config.CreationTimestamp)) } func printShortDestinationRule(config model.Config, w io.Writer) { destinationRule, ok := config.Spec.(*v1alpha3.DestinationRule) if !ok { fmt.Fprintf(w, "Not a destinationrule: %v", config) return } subsets := make([]string, 0) for _, subset := range destinationRule.Subsets { subsets = append(subsets, subset.Name) } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", config.Name, destinationRule.Host, strings.Join(subsets, ","), config.Namespace, renderTimestamp(config.CreationTimestamp)) } func printShortServiceEntry(config model.Config, w io.Writer) { serviceEntry, ok := config.Spec.(*v1alpha3.ServiceEntry) if !ok { fmt.Fprintf(w, "Not a serviceentry: %v", config) return } ports := make([]string, 0) for _, port := range serviceEntry.Ports { ports = append(ports, fmt.Sprintf("%s/%d", port.Protocol, port.Number)) } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", config.Name, strings.Join(serviceEntry.Hosts, ","), strings.Join(ports, ","), config.Namespace, renderTimestamp(config.CreationTimestamp)) } func printShortGateway(config model.Config, w io.Writer) { gateway, ok := config.Spec.(*v1alpha3.Gateway) if !ok { fmt.Fprintf(w, "Not a gateway: %v", config) return } // Determine the servers servers := make(map[string]bool) for _, server := range gateway.Servers { for _, host := range server.Hosts { servers[host] = true } } hosts := make([]string, 0) for host := range servers { hosts = append(hosts, host) } fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", config.Name, strings.Join(hosts, ","), config.Namespace, renderTimestamp(config.CreationTimestamp)) } // Print as YAML func printYamlOutput(writer io.Writer, configClient model.ConfigStore, configList []model.Config) { descriptor := configClient.ConfigDescriptor() for _, config := range configList { schema, exists := descriptor.GetByType(config.Type) if !exists { log.Errorf("Unknown kind %q for %v", crd.ResourceName(config.Type), config.Name) continue } obj, err := crd.ConvertConfig(schema, config) if err != nil { log.Errorf("Could not decode %v: %v", config.Name, err) continue } bytes, err := yaml.Marshal(obj) if err != nil { log.Errorf("Could not convert %v to YAML: %v", config, err) continue } fmt.Fprint(writer, string(bytes)) fmt.Fprintln(writer, "---") } } func newClient() (model.ConfigStore, error) { return crd.NewClient(kubeconfig, configContext, model.IstioConfigTypes, "") } func supportedTypes(configClient model.ConfigStore) []string { types := configClient.ConfigDescriptor().Types() for i := range types { types[i] = crd.ResourceName(types[i]) } return types } func preprocMixerConfig(configs []crd.IstioKind) error { var err error for i, config := range configs { if configs[i].Namespace, err = handleNamespaces(config.Namespace); err != nil { return err } if config.APIVersion == "" { configs[i].APIVersion = legacyIstioAPIGroupVersion.String() } // TODO: invokes the mixer validation webhook. } return nil } func restConfig() (config *rest.Config, err error) { config, err = kubecfg.BuildClientConfig(kubeconfig, configContext) if err != nil { return } config.GroupVersion = &legacyIstioAPIGroupVersion config.APIPath = "/apis" config.ContentType = runtime.ContentTypeJSON types := runtime.NewScheme() schemeBuilder := runtime.NewSchemeBuilder( func(scheme *runtime.Scheme) error { metav1.AddToGroupVersion(scheme, legacyIstioAPIGroupVersion) return nil }) err = schemeBuilder.AddToScheme(types) config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: serializer.NewCodecFactory(types)} return } func apiResources(config *rest.Config, configs []crd.IstioKind) (map[string]metav1.APIResource, error) { client, err := discovery.NewDiscoveryClientForConfig(config) if err != nil { return nil, err } resources, err := client.ServerResourcesForGroupVersion(legacyIstioAPIGroupVersion.String()) if err != nil { return nil, err } kindsSet := map[string]bool{} for _, config := range configs { if !kindsSet[config.Kind] { kindsSet[config.Kind] = true } } result := make(map[string]metav1.APIResource, len(kindsSet)) for _, resource := range resources.APIResources { if kindsSet[resource.Kind] { result[resource.Kind] = resource } } return result, nil } func restClientForOthers(config *rest.Config) (*rest.RESTClient, error) { return rest.RESTClientFor(config) } func prepareClientForOthers(configs []crd.IstioKind) (*rest.RESTClient, map[string]metav1.APIResource, error) { restConfig, err := restConfig() if err != nil { return nil, nil, err } resources, err := apiResources(restConfig, configs) if err != nil { return nil, nil, err } client, err := restClientForOthers(restConfig) if err != nil { return nil, nil, err } return client, resources, nil } func configTypeResourceNames(configTypes model.ConfigDescriptor) []string { resourceNames := make([]string, len(configTypes)) for _, typ := range configTypes { resourceNames = append(resourceNames, crd.ResourceName(typ.Type)) } return resourceNames } func configTypePluralResourceNames(configTypes model.ConfigDescriptor) []string { resourceNames := make([]string, len(configTypes)) for _, typ := range configTypes { resourceNames = append(resourceNames, crd.ResourceName(typ.Plural)) } return resourceNames } // renderTimestamp creates a human-readable age similar to docker and kubectl CLI output func renderTimestamp(ts time.Time) string { if ts.IsZero() { return "<unknown>" } seconds := int(time.Since(ts).Seconds()) if seconds < -2 { return fmt.Sprintf("<invalid>") } else if seconds < 0 { return fmt.Sprintf("0s") } else if seconds < 60 { return fmt.Sprintf("%ds", seconds) } minutes := int(time.Since(ts).Minutes()) if minutes < 60 { return fmt.Sprintf("%dm", minutes) } hours := int(time.Since(ts).Hours()) if hours < 24 { return fmt.Sprintf("%dh", hours) } else if hours < 365*24 { return fmt.Sprintf("%dd", hours/24) } return fmt.Sprintf("%dy", hours/24/365) } func init() { defaultContext := "istio" contextCmd.PersistentFlags().StringVar(&istioContext, "context", defaultContext, "Kubernetes configuration file context name") contextCmd.PersistentFlags().StringVar(&istioAPIServer, "api-server", "", "URL for Istio api server") postCmd.PersistentFlags().StringVarP(&file, "file", "f", "", "Input file with the content of the configuration objects (if not set, command reads from the standard input)") putCmd.PersistentFlags().AddFlag(postCmd.PersistentFlags().Lookup("file")) deleteCmd.PersistentFlags().AddFlag(postCmd.PersistentFlags().Lookup("file")) getCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "short", "Output format. One of:yaml|short") getCmd.PersistentFlags().BoolVar(&getAllNamespaces, "all-namespaces", false, "If present, list the requested object(s) across all namespaces. Namespace in current "+ "context is ignored even if specified with --namespace.") }
// Copyright 2017 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "github.com/spf13/cobra" "istio.io/istio/pilot/pkg/serviceregistry/kube" "istio.io/istio/pkg/log" ) var ( deregisterCmd = &cobra.Command{ Use: "deregister <svcname> <ip>", Short: "De-registers a service instance", Example: `# de-register an endpoint 172.17.0.2 from service my-svc: istioctl deregister my-svc 172.17.0.2`, Args: cobra.MinimumNArgs(2), RunE: func(c *cobra.Command, args []string) error { svcName := args[0] ip := args[1] log.Infof("De-registering for service '%s' ip '%s'", svcName, ip) client, err := createInterface(kubeconfig) if err != nil { return err } ns, _ := handleNamespaces(namespace) return kube.DeRegisterEndpoint(client, ns, svcName, ip) }, } ) func init() { rootCmd.AddCommand(deregisterCmd) }
// Copyright 2017 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gendeployment import ( "fmt" "io/ioutil" "os" "strings" "github.com/spf13/cobra" ) const ( defaultTag = "master-latest-daily" ) // Command returns the "gen-deploy" subcommand for istioctl. func Command(istioNamespaceFlag *string) *cobra.Command { var ( helmChartLocation string valuesPath string ) install := defaultInstall() cmd := &cobra.Command{ Deprecated: "Please use `helm template` instead (see https://istio.io/docs/setup/kubernetes/helm-install/#option-1-install-with-helm-via-helm-template)", // nolint: lll Use: "gen-deploy", Short: "Generates the configuration for Istio's control plane.", Long: "istioctl gen-deploy produces deployment files to run the Istio.", Example: `istioctl gen-deploy --values myvalues.yaml`, RunE: func(c *cobra.Command, args []string) error { install.Namespace = *istioNamespaceFlag // TODO: this is NOT merged with the values.yaml from helm directory. values, err := getValues(valuesPath) if err != nil { return err } rendered, err := yamlFromInstallation(values, *istioNamespaceFlag, helmChartLocation) if err != nil { return err } _, err = fmt.Fprint(os.Stdout, rendered) return err }, } cmd.PersistentFlags().StringVar(&valuesPath, "values", "", "Path to the Helm values.yaml file used to render YAML "+ "deployments locally when --out=yaml. Flag values are ignored in favor of using the file directly.") cmd.PersistentFlags().StringVar(&install.Hub, "hub", install.Hub, "The container registry to pull Istio images from") cmd.PersistentFlags().StringVar(&install.MixerTag, "mixer-tag", install.MixerTag, "The tag to use to pull the `mixer` container") cmd.PersistentFlags().StringVar(&install.PilotTag, "pilot-tag", install.PilotTag, "The tag to use to pull the `pilot-discovery` container") cmd.PersistentFlags().StringVar(&install.CaTag, "ca-tag", install.CaTag, "The tag to use to pull the `ca` container") cmd.PersistentFlags().StringVar(&install.ProxyTag, "proxy-tag", install.ProxyTag, "The tag to use to pull the `proxy` container") cmd.PersistentFlags().BoolVar(&install.Debug, "debug", install.Debug, "If true, uses debug images instead of release images") cmd.PersistentFlags().Uint16Var(&install.NodePort, "ingress-node-port", install.NodePort, "If provided, Istio ingress proxies will run as a NodePort service mapped to the port provided by this flag. "+ "Note that this flag is ignored unless the \"ingress\" feature flag is provided too.") // TODO: figure out how we want to package up the charts with the binary to make this easy cmd.PersistentFlags().StringVar(&helmChartLocation, "helm-chart-dir", ".", "The directory to find the helm charts used to render Istio deployments. -o yaml uses these to render the helm chart locally.") _ = cmd.PersistentFlags().MarkHidden("hub") _ = cmd.PersistentFlags().MarkHidden("mixer-tag") _ = cmd.PersistentFlags().MarkHidden("pilot-tag") _ = cmd.PersistentFlags().MarkHidden("ca-tag") _ = cmd.PersistentFlags().MarkHidden("proxy-tag") return cmd } func getValues(path string) (string, error) { if path == "" { return "", nil } out, err := ioutil.ReadFile(path) if err != nil { return "", err } return string(out), nil } type installation struct { Namespace string // todo: support hub per component Hub string // hub to pull images from MixerTag string PilotTag string CaTag string ProxyTag string NodePort uint16 Debug bool Mixer bool Pilot bool CA bool Ingress bool SidecarInjector bool } func defaultInstall() *installation { return &installation{ Mixer: true, Pilot: true, CA: true, Ingress: true, SidecarInjector: false, Namespace: "istio-system", Debug: false, NodePort: 0, Hub: "gcr.io/istio-release", MixerTag: defaultTag, PilotTag: defaultTag, CaTag: defaultTag, ProxyTag: defaultTag, } } func (i *installation) setFeatures(features []string) error { if len(features) == 0 { return nil } else if len(features) == 1 { features = strings.Split(features[0], ",") } i.Mixer = false i.Pilot = false i.CA = false i.Ingress = false i.SidecarInjector = false for _, f := range features { switch strings.ToLower(f) { case "telemetry", "policy": i.Mixer = true i.Pilot = true case "routing": i.Pilot = true case "mtls": i.CA = true i.Pilot = true case "ingress": i.Ingress = true i.Pilot = true case "sidecar-injector": i.SidecarInjector = true default: return fmt.Errorf("invalid feature name %q", f) } } return nil }
// Copyright 2017 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gendeployment import ( "bytes" "fmt" "strings" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/engine" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/timeconv" ) // TODO: add tests based on golden files. Need to fix up helm charts first, since they're not correct atm. // Today they spit out multiple auth deployments, we need to fix that then we can build golden outputs. func yamlFromInstallation(values, namespace, helmChartDirectory string) (string, error) { c, err := chartutil.Load(helmChartDirectory) if err != nil { return "", err } config := &chart.Config{Raw: values, Values: map[string]*chart.Value{}} options := chartutil.ReleaseOptions{ Name: "istio", Time: timeconv.Now(), Namespace: namespace, } vals, err := chartutil.ToRenderValues(c, config, options) if err != nil { return "", err } files, err := engine.New().Render(c, vals) if err != nil { return "", err } out := &bytes.Buffer{} for name, data := range files { if len(strings.TrimSpace(data)) == 0 { continue } if _, err = fmt.Fprintf(out, "---\n# Source: %q\n", name); err != nil { return "", err } if _, err = fmt.Fprintln(out, data); err != nil { return "", err } } return out.String(), nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "strings" ) // Uses proxyName to infer namespace if the passed proxyName contains namespace information. // Otherwise uses the namespace value passed into the function func inferPodInfo(proxyName, namespace string) (string, string) { parsedProxy := strings.Split(proxyName, ".") if len(parsedProxy) == 1 { return proxyName, namespace } return parsedProxy[0], parsedProxy[1] }
// Copyright 2017 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "errors" "fmt" "io" "io/ioutil" "os" "github.com/ghodss/yaml" "github.com/spf13/cobra" "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" meshconfig "istio.io/api/mesh/v1alpha1" "istio.io/istio/pilot/cmd" "istio.io/istio/pilot/pkg/kube/inject" "istio.io/istio/pilot/pkg/model" "istio.io/istio/pkg/env" "istio.io/istio/pkg/kube" "istio.io/istio/pkg/log" "istio.io/istio/pkg/version" ) const ( configMapKey = "mesh" injectConfigMapKey = "config" ) func createInterface(kubeconfig string) (kubernetes.Interface, error) { restConfig, err := kube.BuildClientConfig(kubeconfig, configContext) if err != nil { return nil, err } return kubernetes.NewForConfig(restConfig) } func getMeshConfigFromConfigMap(kubeconfig string) (*meshconfig.MeshConfig, error) { client, err := createInterface(kubeconfig) if err != nil { return nil, err } config, err := client.CoreV1().ConfigMaps(istioNamespace).Get(meshConfigMapName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("could not read valid configmap %q from namespace %q: %v - "+ "Use --meshConfigFile or re-run kube-inject with `-i <istioSystemNamespace> and ensure valid MeshConfig exists", meshConfigMapName, istioNamespace, err) } // values in the data are strings, while proto might use a // different data type. therefore, we have to get a value by a // key configYaml, exists := config.Data[configMapKey] if !exists { return nil, fmt.Errorf("missing configuration map key %q", configMapKey) } cfg, err := model.ApplyMeshConfigDefaults(configYaml) if err != nil { err = multierr.Append(fmt.Errorf("istioctl version %s cannot parse mesh config. Install istioctl from the latest Istio release", version.Info.Version), err) } return cfg, err } func getInjectConfigFromConfigMap(kubeconfig string) (string, error) { client, err := createInterface(kubeconfig) if err != nil { return "", err } config, err := client.CoreV1().ConfigMaps(istioNamespace).Get(injectConfigMapName, metav1.GetOptions{}) if err != nil { return "", fmt.Errorf("could not find valid configmap %q from namespace %q: %v - "+ "Use --injectConfigFile or re-run kube-inject with `-i <istioSystemNamespace> and ensure istio-inject configmap exists", injectConfigMapName, istioNamespace, err) } // values in the data are strings, while proto might use a // different data type. therefore, we have to get a value by a // key injectData, exists := config.Data[injectConfigMapKey] if !exists { return "", fmt.Errorf("missing configuration map key %q in %q", injectConfigMapKey, injectConfigMapName) } var injectConfig inject.Config if err := yaml.Unmarshal([]byte(injectData), &injectConfig); err != nil { return "", fmt.Errorf("unable to convert data from configmap %q: %v", injectConfigMapName, err) } log.Debugf("using inject template from configmap %q", injectConfigMapName) return injectConfig.Template, nil } func validateFlags() error { var err error if inFilename != "" && emitTemplate { err = multierr.Append(err, errors.New("--filename and --emitTemplate are mutually exclusive")) } if inFilename == "" && !emitTemplate { err = multierr.Append(err, errors.New("filename not specified (see --filename or -f)")) } if meshConfigFile == "" && meshConfigMapName == "" { err = multierr.Append(err, errors.New("--meshConfigFile or --meshConfigMapName must be set")) } err = multierr.Append(err, inject.ValidateIncludeIPRanges(includeIPRanges)) err = multierr.Append(err, inject.ValidateExcludeIPRanges(excludeIPRanges)) err = multierr.Append(err, inject.ValidateIncludeInboundPorts(includeInboundPorts)) err = multierr.Append(err, inject.ValidateExcludeInboundPorts(excludeInboundPorts)) return err } var ( hub string tag string sidecarProxyUID uint64 verbosity int versionStr string // override build version enableCoreDump bool rewriteAppHTTPProbe bool imagePullPolicy string statusPort int readinessInitialDelaySeconds uint32 readinessPeriodSeconds uint32 readinessFailureThreshold uint32 includeIPRanges string excludeIPRanges string includeInboundPorts string excludeInboundPorts string debugMode bool emitTemplate bool inFilename string outFilename string meshConfigFile string meshConfigMapName string injectConfigFile string injectConfigMapName string ) var ( useBuiltinDefaultsVar = env.RegisterBoolVar("ISTIOCTL_USE_BUILTIN_DEFAULTS", false, "") injectCmd = &cobra.Command{ Use: "kube-inject", Short: "Inject Envoy sidecar into Kubernetes pod resources", Long: ` kube-inject manually injects the Envoy sidecar into Kubernetes workloads. Unsupported resources are left unmodified so it is safe to run kube-inject over a single file that contains multiple Service, ConfigMap, Deployment, etc. definitions for a complex application. Its best to do this when the resource is initially created. k8s.io/docs/concepts/workloads/pods/pod-overview/#pod-templates is updated for Job, DaemonSet, ReplicaSet, Pod and Deployment YAML resource documents. Support for additional pod-based resource types can be added as necessary. The Istio project is continually evolving so the Istio sidecar configuration may change unannounced. When in doubt re-run istioctl kube-inject on deployments to get the most up-to-date changes. To override the sidecar injection template built into istioctl, the parameters --injectConfigFile or --injectConfigMapName can be used. Both options override any other template configuration parameters, eg. --hub and --tag. These options would typically be used with the file/configmap created with a new Istio release. `, Example: ` # Update resources on the fly before applying. kubectl apply -f <(istioctl kube-inject -f <resource.yaml>) # Create a persistent version of the deployment with Envoy sidecar # injected. istioctl kube-inject -f deployment.yaml -o deployment-injected.yaml # Update an existing deployment. kubectl get deployment -o yaml | istioctl kube-inject -f - | kubectl apply -f - # Create a persistent version of the deployment with Envoy sidecar # injected configuration from Kubernetes configmap 'istio-inject' istioctl kube-inject -f deployment.yaml -o deployment-injected.yaml --injectConfigMapName istio-inject `, RunE: func(c *cobra.Command, _ []string) (err error) { if err = validateFlags(); err != nil { return err } var reader io.Reader if !emitTemplate { if inFilename == "-" { reader = os.Stdin } else { var in *os.File if in, err = os.Open(inFilename); err != nil { return err } reader = in defer func() { if errClose := in.Close(); errClose != nil { log.Errorf("Error: close file from %s, %s", inFilename, errClose) // don't overwrite the previous error if err == nil { err = errClose } } }() } } var writer io.Writer if outFilename == "" { writer = c.OutOrStdout() } else { var out *os.File if out, err = os.Create(outFilename); err != nil { return err } writer = out defer func() { if errClose := out.Close(); errClose != nil { log.Errorf("Error: close file from %s, %s", outFilename, errClose) // don't overwrite the previous error if err == nil { err = errClose } } }() } if versionStr == "" { versionStr = version.Info.String() } var meshConfig *meshconfig.MeshConfig if meshConfigFile != "" { if meshConfig, err = cmd.ReadMeshConfig(meshConfigFile); err != nil { return err } } else { if meshConfig, err = getMeshConfigFromConfigMap(kubeconfig); err != nil { return err } } var sidecarTemplate string // hub and tag params only work with ISTIOCTL_USE_BUILTIN_DEFAULTS // so must be specified together. hub and tag no longer have defaults. if hub != "" || tag != "" { // ISTIOCTL_USE_BUILTIN_DEFAULTS is used to have legacy behavior. if !useBuiltinDefaultsVar.Get() { return errors.New("one of injectConfigFile or injectConfigMapName is required\n" + "use the following command to get the current injector file\n" + "kubectl -n istio-system get configmap istio-sidecar-injector " + "-o=jsonpath='{.data.config}' > /tmp/injectConfigFile.yaml") } if hub == "" || tag == "" { return fmt.Errorf("hub and tag are both required. got hub: '%v', tag: '%v'", hub, tag) } if sidecarTemplate, err = inject.GenerateTemplateFromParams(&inject.Params{ InitImage: inject.InitImageName(hub, tag, debugMode), ProxyImage: inject.ProxyImageName(hub, tag, debugMode), RewriteAppHTTPProbe: rewriteAppHTTPProbe, Verbosity: verbosity, SidecarProxyUID: sidecarProxyUID, Version: versionStr, EnableCoreDump: enableCoreDump, Mesh: meshConfig, ImagePullPolicy: imagePullPolicy, StatusPort: statusPort, ReadinessInitialDelaySeconds: readinessInitialDelaySeconds, ReadinessPeriodSeconds: readinessPeriodSeconds, ReadinessFailureThreshold: readinessFailureThreshold, IncludeIPRanges: includeIPRanges, ExcludeIPRanges: excludeIPRanges, IncludeInboundPorts: includeInboundPorts, ExcludeInboundPorts: excludeInboundPorts, DebugMode: debugMode, }); err != nil { return err } } else if injectConfigFile != "" { injectionConfig, err := ioutil.ReadFile(injectConfigFile) // nolint: vetshadow if err != nil { return err } var config inject.Config if err := yaml.Unmarshal(injectionConfig, &config); err != nil { return err } sidecarTemplate = config.Template } else { if sidecarTemplate, err = getInjectConfigFromConfigMap(kubeconfig); err != nil { return err } } if emitTemplate { config := inject.Config{ Policy: inject.InjectionPolicyEnabled, Template: sidecarTemplate, } out, err := yaml.Marshal(&config) if err != nil { return err } fmt.Println(string(out)) return nil } return inject.IntoResourceFile(sidecarTemplate, meshConfig, reader, writer) }, } ) const ( defaultMeshConfigMapName = "istio" defaultInjectConfigMapName = "istio-sidecar-injector" ) func init() { rootCmd.AddCommand(injectCmd) injectCmd.PersistentFlags().StringVar(&hub, "hub", "", "Docker hub") injectCmd.PersistentFlags().StringVar(&tag, "tag", "", "Docker tag") injectCmd.PersistentFlags().StringVar(&meshConfigFile, "meshConfigFile", "", "mesh configuration filename. Takes precedence over --meshConfigMapName if set") injectCmd.PersistentFlags().StringVar(&injectConfigFile, "injectConfigFile", "", "injection configuration filename. Cannot be used with --injectConfigMapName") injectCmd.PersistentFlags().BoolVar(&emitTemplate, "emitTemplate", false, "Emit sidecar template based on parameterized flags") _ = injectCmd.PersistentFlags().MarkHidden("emitTemplate") injectCmd.PersistentFlags().StringVarP(&inFilename, "filename", "f", "", "Input Kubernetes resource filename") injectCmd.PersistentFlags().StringVarP(&outFilename, "output", "o", "", "Modified output Kubernetes resource filename") injectCmd.PersistentFlags().IntVar(&verbosity, "verbosity", inject.DefaultVerbosity, "Runtime verbosity") injectCmd.PersistentFlags().Uint64Var(&sidecarProxyUID, "sidecarProxyUID", inject.DefaultSidecarProxyUID, "Envoy sidecar UID") injectCmd.PersistentFlags().StringVar(&versionStr, "setVersionString", "", "Override version info injected into resource") // Default --coreDump=true for pre-alpha development. Core dump // settings (i.e. sysctl kernel.*) affect all pods in a node and // require privileges. This option should only be used by the cluster // admin (see https://kubernetes.io/docs/concepts/cluster-administration/sysctl-cluster/) // injector specific params are deprecated injectCmd.PersistentFlags().BoolVar(&enableCoreDump, "coreDump", true, "Enable/Disable core dumps in injected Envoy sidecar (--coreDump=true affects "+ "all pods in a node and should only be used the cluster admin)") // TODO(incfly): deprecate this flag once hardcoded injection template is gone. By then, everything // comes from configmap injector, whose template already contains rewriteAppHTTPProbe control switch. injectCmd.PersistentFlags().BoolVar(&rewriteAppHTTPProbe, "rewriteAppProbe", false, "Whether injector "+ "rewrites the liveness health check to let kubelet health check the app when mtls is on.") injectCmd.PersistentFlags().StringVar(&imagePullPolicy, "imagePullPolicy", inject.DefaultImagePullPolicy, "Sets the container image pull policy. Valid options are Always,IfNotPresent,Never."+ "The default policy is IfNotPresent.") injectCmd.PersistentFlags().IntVar(&statusPort, inject.StatusPortCmdFlagName, inject.DefaultStatusPort, "HTTP Port on which to serve pilot agent status. The path /healthz/ can be used for health checking. "+ "If zero, agent status will not be provided.") injectCmd.PersistentFlags().Uint32Var(&readinessInitialDelaySeconds, "readinessInitialDelaySeconds", inject.DefaultReadinessInitialDelaySeconds, "The initial delay (in seconds) for the readiness probe.") injectCmd.PersistentFlags().Uint32Var(&readinessPeriodSeconds, "readinessPeriodSeconds", inject.DefaultReadinessPeriodSeconds, "The period between readiness probes (in seconds).") injectCmd.PersistentFlags().Uint32Var(&readinessFailureThreshold, "readinessFailureThreshold", inject.DefaultReadinessFailureThreshold, "The threshold for successive failed readiness probes.") injectCmd.PersistentFlags().StringVar(&includeIPRanges, "includeIPRanges", inject.DefaultIncludeIPRanges, "Comma separated list of IP ranges in CIDR form. If set, only redirect outbound traffic to Envoy for "+ "these IP ranges. All outbound traffic can be redirected with the wildcard character '*'.") injectCmd.PersistentFlags().StringVar(&excludeIPRanges, "excludeIPRanges", "", "Comma separated list of IP ranges in CIDR form. If set, outbound traffic will not be redirected for "+ "these IP ranges. Exclusions are only applied if configured to redirect all outbound traffic. By "+ "default, no IP ranges are excluded.") injectCmd.PersistentFlags().StringVar(&includeInboundPorts, "includeInboundPorts", inject.DefaultIncludeInboundPorts, "Comma separated list of inbound ports for which traffic is to be redirected to Envoy. All ports can "+ "be redirected with the wildcard character '*'.") injectCmd.PersistentFlags().StringVar(&excludeInboundPorts, "excludeInboundPorts", "", "Comma separated list of inbound ports. If set, inbound traffic will not be redirected for those "+ "ports. Exclusions are only applied if configured to redirect all inbound traffic. By default, no ports "+ "are excluded.") injectCmd.PersistentFlags().BoolVar(&debugMode, "debug", false, "Use debug images and settings for the sidecar") deprecatedFlags := []string{"coreDump", "imagePullPolicy", "includeIPRanges", "excludeIPRanges", "hub", "tag", "includeInboundPorts", "excludeInboundPorts", "debug", "verbosity", "sidecarProxyUID", "setVersionString"} for _, opt := range deprecatedFlags { _ = injectCmd.PersistentFlags().MarkDeprecated(opt, "Use --injectConfigMapName or --injectConfigFile instead") } injectCmd.PersistentFlags().StringVar(&meshConfigMapName, "meshConfigMapName", defaultMeshConfigMapName, fmt.Sprintf("ConfigMap name for Istio mesh configuration, key should be %q", configMapKey)) injectCmd.PersistentFlags().StringVar(&injectConfigMapName, "injectConfigMapName", defaultInjectConfigMapName, fmt.Sprintf("ConfigMap name for Istio sidecar injection, key should be %q."+ "This option overrides any other sidecar injection config options, eg. --hub", injectConfigMapKey)) }
// Copyright 2017 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "os" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" v1 "k8s.io/api/core/v1" // import all known client auth plugins _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" "istio.io/istio/istioctl/cmd/istioctl/gendeployment" "istio.io/istio/istioctl/pkg/install" "istio.io/istio/istioctl/pkg/validate" "istio.io/istio/pilot/pkg/serviceregistry/kube" "istio.io/istio/pkg/cmd" "istio.io/istio/pkg/collateral" "istio.io/istio/pkg/log" "istio.io/istio/pkg/version" ) const ( kubePlatform = "kube" ) var ( platform string kubeconfig string configContext string namespace string istioNamespace string defaultNamespace string // input file name file string // output format (yaml or short) outputFormat string // Create a kubernetes.ExecClient (or mockExecClient) clientExecFactory = newExecClient loggingOptions = log.DefaultOptions() rootCmd = &cobra.Command{ Use: "istioctl", Short: "Istio control interface.", SilenceUsage: true, DisableAutoGenTag: true, Long: `Istio configuration command line utility for service operators to debug and diagnose their Istio mesh. `, PersistentPreRunE: istioPersistentPreRunE, } experimentalCmd = &cobra.Command{ Use: "experimental", Short: "Experimental commands that may be modified or deprecated", } ) func istioPersistentPreRunE(_ *cobra.Command, _ []string) error { if err := log.Configure(loggingOptions); err != nil { return err } defaultNamespace = getDefaultNamespace(kubeconfig) return nil } func init() { rootCmd.PersistentFlags().StringVarP(&platform, "platform", "p", kubePlatform, "Istio host platform") rootCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "c", "", "Kubernetes configuration file") rootCmd.PersistentFlags().StringVar(&configContext, "context", "", "The name of the kubeconfig context to use") rootCmd.PersistentFlags().StringVarP(&istioNamespace, "istioNamespace", "i", kube.IstioNamespace, "Istio system namespace") rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", v1.NamespaceAll, "Config namespace") // Attach the Istio logging options to the command. loggingOptions.AttachCobraFlags(rootCmd) hiddenFlags := []string{"log_as_json", "log_rotate", "log_rotate_max_age", "log_rotate_max_backups", "log_rotate_max_size", "log_stacktrace_level", "log_target", "log_caller"} for _, opt := range hiddenFlags { _ = rootCmd.PersistentFlags().MarkHidden(opt) } cmd.AddFlags(rootCmd) rootCmd.AddCommand(version.CobraCommandWithOptions(version.CobraOptions{GetRemoteVersion: getRemoteInfo})) rootCmd.AddCommand(gendeployment.Command(&istioNamespace)) experimentalCmd.AddCommand(install.NewVerifyCommand(&istioNamespace)) experimentalCmd.AddCommand(Rbac()) rootCmd.AddCommand(experimentalCmd) rootCmd.AddCommand(collateral.CobraCommand(rootCmd, &doc.GenManHeader{ Title: "Istio Control", Section: "istioctl CLI", Manual: "Istio Control", })) // Deprecated commands rootCmd.AddCommand(postCmd) rootCmd.AddCommand(putCmd) rootCmd.AddCommand(getCmd) rootCmd.AddCommand(deleteCmd) rootCmd.AddCommand(contextCmd) rootCmd.AddCommand(validate.NewValidateCommand()) } func getRemoteInfo() (*version.MeshInfo, error) { kubeClient, err := clientExecFactory(kubeconfig, configContext) if err != nil { return nil, err } return kubeClient.GetIstioVersions(istioNamespace) } func main() { if platform != kubePlatform { log.Warnf("Platform '%s' not supported.", platform) } if err := rootCmd.Execute(); err != nil { os.Exit(1) } } func getDefaultNamespace(kubeconfig string) string { configAccess := clientcmd.NewDefaultPathOptions() if kubeconfig != "" { // use specified kubeconfig file for the location of the // config to read configAccess.GlobalFile = kubeconfig } // gets existing kubeconfig or returns new empty config config, err := configAccess.GetStartingConfig() if err != nil { return v1.NamespaceDefault } context, ok := config.Contexts[config.CurrentContext] if !ok { return v1.NamespaceDefault } if context.Namespace == "" { return v1.NamespaceDefault } return context.Namespace } func handleNamespaces(objectNamespace string) (string, error) { if objectNamespace != "" && namespace != "" && namespace != objectNamespace { return "", fmt.Errorf(`the namespace from the provided object "%s" does `+ `not match the namespace "%s". You must pass '--namespace=%s' to perform `+ `this operation`, objectNamespace, namespace, objectNamespace) } if namespace != "" { return namespace, nil } if objectNamespace != "" { return objectNamespace, nil } return defaultNamespace, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "errors" "fmt" "io/ioutil" "net" "net/http" "os" "strings" "text/tabwriter" "time" multierror "github.com/hashicorp/go-multierror" "github.com/prometheus/client_golang/api" promv1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/common/model" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/portforward" "k8s.io/client-go/transport/spdy" "istio.io/istio/istioctl/pkg/kubernetes" "istio.io/istio/pkg/log" ) var ( metricsCmd = &cobra.Command{ Use: "metrics <workload name>...", Short: "Prints the metrics for the specified workload(s) when running in Kubernetes.", Long: ` Prints the metrics for the specified service(s) when running in Kubernetes. This command finds a Prometheus pod running in the specified istio system namespace. It then executes a series of queries per requested workload to find the following top-level workload metrics: total requests per second, error rate, and request latency at p50, p90, and p99 percentiles. The query results are printed to the console, organized by workload name. All metrics returned are from server-side reports. This means that latencies and error rates are from the perspective of the service itself and not of an individual client (or aggregate set of clients). Rates and latencies are calculated over a time interval of 1 minute. `, Example: ` # Retrieve workload metrics for productpage-v1 workload istioctl experimental metrics productpage-v1 # Retrieve workload metrics for various services in the different namespaces istioctl experimental metrics productpage-v1.foo reviews-v1.bar ratings-v1.baz `, Aliases: []string{"m"}, Args: cobra.MinimumNArgs(1), RunE: run, DisableFlagsInUseLine: true, } ) const ( wlabel = "destination_workload" wnslabel = "destination_workload_namespace" reqTot = "istio_requests_total" reqDur = "istio_request_duration_seconds" ) func init() { experimentalCmd.AddCommand(metricsCmd) } type workloadMetrics struct { workload string totalRPS, errorRPS float64 p50Latency, p90Latency, p99Latency time.Duration } func run(_ *cobra.Command, args []string) error { log.Debugf("metrics command invoked for workload(s): %v", args) client, err := kubernetes.NewClient(kubeconfig, configContext) if err != nil { return fmt.Errorf("failed to create k8s client: %v", err) } pl, err := prometheusPods(client) if err != nil { return fmt.Errorf("not able to locate prometheus pod: %v", err) } if len(pl.Items) < 1 { return errors.New("no prometheus pods found") } port, err := availablePort() if err != nil { return fmt.Errorf("not able to find available port: %v", err) } log.Debugf("Using local port: %d", port) // only use the first pod in the list promPod := pl.Items[0] fw, readyCh, err := buildPortForwarder(client, client.Config, promPod.Name, port) if err != nil { return fmt.Errorf("could not build port forwarder for prometheus: %v", err) } errCh := make(chan error) go func() { errCh <- fw.ForwardPorts() }() select { case err := <-errCh: return fmt.Errorf("failure running port forward process: %v", err) case <-readyCh: log.Debugf("port-forward to prometheus pod ready") defer fw.Close() promAPI, err := prometheusAPI(port) if err != nil { return err } printHeader() workloads := args for _, workload := range workloads { sm, err := metrics(promAPI, workload) if err != nil { return fmt.Errorf("could not build metrics for workload '%s': %v", workload, err) } printMetrics(sm) } return nil } } func prometheusPods(client cache.Getter) (*v1.PodList, error) { podGet := client.Get().Resource("pods").Namespace(istioNamespace).Param("labelSelector", "app=prometheus") obj, err := podGet.Do().Get() if err != nil { return nil, fmt.Errorf("failed retrieving pod: %v", err) } return obj.(*v1.PodList), nil } func buildPortForwarder(client *kubernetes.Client, config *rest.Config, podName string, port int) (*portforward.PortForwarder, <-chan struct{}, error) { req := client.Post().Resource("pods").Namespace(istioNamespace).Name(podName).SubResource("portforward") transport, upgrader, err := spdy.RoundTripperFor(config) if err != nil { return nil, nil, fmt.Errorf("failure creating roundtripper: %v", err) } dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) stop := make(chan struct{}) ready := make(chan struct{}) fw, err := portforward.New(dialer, []string{fmt.Sprintf("%d:9090", port)}, stop, ready, ioutil.Discard, os.Stderr) if err != nil { return nil, nil, fmt.Errorf("failed establishing port-forward: %v", err) } return fw, ready, nil } func availablePort() (int, error) { addr, err := net.ResolveTCPAddr("tcp", ":0") if err != nil { return 0, err } l, err := net.ListenTCP("tcp", addr) if err != nil { return 0, err } port := l.Addr().(*net.TCPAddr).Port return port, l.Close() } func prometheusAPI(port int) (promv1.API, error) { promClient, err := api.NewClient(api.Config{Address: fmt.Sprintf("http://localhost:%d", port)}) if err != nil { return nil, fmt.Errorf("could not build prometheus client: %v", err) } return promv1.NewAPI(promClient), nil } func metrics(promAPI promv1.API, workload string) (workloadMetrics, error) { parts := strings.Split(workload, ".") wname := parts[0] wns := "" if len(parts) > 1 { wns = parts[1] } rpsQuery := fmt.Sprintf(`sum(rate(%s{%s=~"%s.*", %s=~"%s.*",reporter="destination"}[1m]))`, reqTot, wlabel, wname, wnslabel, wns) errRPSQuery := fmt.Sprintf(`sum(rate(%s{%s=~"%s.*", %s=~"%s.*",reporter="destination",response_code!="200"}[1m]))`, reqTot, wlabel, wname, wnslabel, wns) p50LatencyQuery := fmt.Sprintf(`histogram_quantile(%f, sum(rate(%s_bucket{%s=~"%s.*", %s=~"%s.*",reporter="destination"}[1m])) by (le))`, 0.5, reqDur, wlabel, wname, wnslabel, wns) p90LatencyQuery := fmt.Sprintf(`histogram_quantile(%f, sum(rate(%s_bucket{%s=~"%s.*", %s=~"%s.*",reporter="destination"}[1m])) by (le))`, 0.9, reqDur, wlabel, wname, wnslabel, wns) p99LatencyQuery := fmt.Sprintf(`histogram_quantile(%f, sum(rate(%s_bucket{%s=~"%s.*", %s=~"%s.*",reporter="destination"}[1m])) by (le))`, 0.99, reqDur, wlabel, wname, wnslabel, wns) var me *multierror.Error var err error sm := workloadMetrics{workload: workload} sm.totalRPS, err = vectorValue(promAPI, rpsQuery) if err != nil { me = multierror.Append(me, err) } sm.errorRPS, err = vectorValue(promAPI, errRPSQuery) if err != nil { me = multierror.Append(me, err) } p50Latency, err := vectorValue(promAPI, p50LatencyQuery) if err != nil { me = multierror.Append(me, err) } sm.p50Latency = time.Duration(p50Latency*1000) * time.Millisecond p90Latency, err := vectorValue(promAPI, p90LatencyQuery) if err != nil { me = multierror.Append(me, err) } sm.p90Latency = time.Duration(p90Latency*1000) * time.Millisecond p99Latency, err := vectorValue(promAPI, p99LatencyQuery) if err != nil { me = multierror.Append(me, err) } sm.p99Latency = time.Duration(p99Latency*1000) * time.Millisecond if me.ErrorOrNil() != nil { return sm, fmt.Errorf("error retrieving some metrics: %v", me.Error()) } return sm, nil } func vectorValue(promAPI promv1.API, query string) (float64, error) { log.Debugf("executing query: %s", query) val, err := promAPI.Query(context.Background(), query, time.Now()) if err != nil { return 0, fmt.Errorf("query() failure for '%s': %v", query, err) } switch v := val.(type) { case model.Vector: if v.Len() < 1 { log.Debugf("no values for query: %s", query) return 0, nil } return float64(v[0].Value), nil default: return 0, errors.New("bad metric value type returned for query") } } func printHeader() { w := tabwriter.NewWriter(os.Stdout, 13, 1, 2, ' ', tabwriter.AlignRight) fmt.Fprintf(w, "%40s\tTOTAL RPS\tERROR RPS\tP50 LATENCY\tP90 LATENCY\tP99 LATENCY\t\n", "WORKLOAD") _ = w.Flush() } func printMetrics(wm workloadMetrics) { w := tabwriter.NewWriter(os.Stdout, 13, 1, 2, ' ', tabwriter.AlignRight) fmt.Fprintf(w, "%40s\t", wm.workload) fmt.Fprintf(w, "%.3f\t", wm.totalRPS) fmt.Fprintf(w, "%.3f\t", wm.errorRPS) fmt.Fprintf(w, "%s\t", wm.p50Latency) fmt.Fprintf(w, "%s\t", wm.p90Latency) fmt.Fprintf(w, "%s\t\n", wm.p99Latency) _ = w.Flush() }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "io" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" "istio.io/istio/istioctl/pkg/writer/envoy/clusters" "istio.io/istio/istioctl/pkg/writer/envoy/configdump" "istio.io/istio/pilot/pkg/model" ) const ( jsonOutput = "json" summaryOutput = "short" ) var ( configCmd = &cobra.Command{ Use: "proxy-config", Short: "Retrieve information about proxy configuration from Envoy [kube only]", Long: `A group of commands used to retrieve information about proxy configuration from the Envoy config dump`, Example: ` # Retrieve information about proxy configuration from an Envoy instance. istioctl proxy-config <clusters|listeners|routes|endpoints|bootstrap> <pod-name[.namespace]>`, Aliases: []string{"pc"}, } fqdn, direction, subset string port int clusterConfigCmd = &cobra.Command{ Use: "cluster <pod-name[.namespace]>", Short: "Retrieves cluster configuration for the Envoy in the specified pod", Long: `Retrieve information about cluster configuration for the Envoy instance in the specified pod.`, Example: ` # Retrieve summary about cluster configuration for a given pod from Envoy. istioctl proxy-config clusters <pod-name[.namespace]> # Retrieve cluster summary for clusters with port 9080. istioctl proxy-config clusters <pod-name[.namespace]> --port 9080 # Retrieve full cluster dump for clusters that are inbound with a FQDN of details.default.svc.cluster.local. istioctl proxy-config clusters <pod-name[.namespace]> --fqdn details.default.svc.cluster.local --direction inbound -o json `, Aliases: []string{"clusters", "c"}, Args: cobra.ExactArgs(1), RunE: func(c *cobra.Command, args []string) error { podName, ns := inferPodInfo(args[0], handleNamespace()) configWriter, err := setupConfigdumpEnvoyConfigWriter(podName, ns, c.OutOrStdout()) if err != nil { return err } filter := configdump.ClusterFilter{ FQDN: model.Hostname(fqdn), Port: port, Subset: subset, Direction: model.TrafficDirection(direction), } switch outputFormat { case summaryOutput: return configWriter.PrintClusterSummary(filter) case jsonOutput: return configWriter.PrintClusterDump(filter) default: return fmt.Errorf("output format %q not supported", outputFormat) } }, } address, listenerType string listenerConfigCmd = &cobra.Command{ Use: "listener <pod-name[.namespace]>", Short: "Retrieves listener configuration for the Envoy in the specified pod", Long: `Retrieve information about listener configuration for the Envoy instance in the specified pod.`, Example: ` # Retrieve summary about listener configuration for a given pod from Envoy. istioctl proxy-config listeners <pod-name[.namespace]> # Retrieve listener summary for listeners with port 9080. istioctl proxy-config listeners <pod-name[.namespace]> --port 9080 # Retrieve full listener dump for HTTP listeners with a wildcard address (0.0.0.0). istioctl proxy-config listeners <pod-name[.namespace]> --type HTTP --address 0.0.0.0 -o json `, Aliases: []string{"listeners", "l"}, Args: cobra.ExactArgs(1), RunE: func(c *cobra.Command, args []string) error { podName, ns := inferPodInfo(args[0], handleNamespace()) configWriter, err := setupConfigdumpEnvoyConfigWriter(podName, ns, c.OutOrStdout()) if err != nil { return err } filter := configdump.ListenerFilter{ Address: address, Port: uint32(port), Type: listenerType, } switch outputFormat { case summaryOutput: return configWriter.PrintListenerSummary(filter) case jsonOutput: return configWriter.PrintListenerDump(filter) default: return fmt.Errorf("output format %q not supported", outputFormat) } }, } routeName string routeConfigCmd = &cobra.Command{ Use: "route <pod-name[.namespace]>", Short: "Retrieves route configuration for the Envoy in the specified pod", Long: `Retrieve information about route configuration for the Envoy instance in the specified pod.`, Example: ` # Retrieve summary about route configuration for a given pod from Envoy. istioctl proxy-config routes <pod-name[.namespace]> # Retrieve route summary for route 9080. istioctl proxy-config route <pod-name[.namespace]> --name 9080 # Retrieve full route dump for route 9080 istioctl proxy-config route <pod-name[.namespace]> --name 9080 -o json `, Aliases: []string{"routes", "r"}, Args: cobra.ExactArgs(1), RunE: func(c *cobra.Command, args []string) error { podName, ns := inferPodInfo(args[0], handleNamespace()) configWriter, err := setupConfigdumpEnvoyConfigWriter(podName, ns, c.OutOrStdout()) if err != nil { return err } filter := configdump.RouteFilter{ Name: routeName, } switch outputFormat { case summaryOutput: return configWriter.PrintRouteSummary(filter) case jsonOutput: return configWriter.PrintRouteDump(filter) default: return fmt.Errorf("output format %q not supported", outputFormat) } }, } bootstrapConfigCmd = &cobra.Command{ Use: "bootstrap <pod-name[.namespace]>", Short: "Retrieves bootstrap configuration for the Envoy in the specified pod", Long: `Retrieve information about bootstrap configuration for the Envoy instance in the specified pod.`, Example: ` # Retrieve full bootstrap configuration for a given pod from Envoy. istioctl proxy-config bootstrap <pod-name[.namespace]> `, Aliases: []string{"b"}, Args: cobra.ExactArgs(1), RunE: func(c *cobra.Command, args []string) error { podName, ns := inferPodInfo(args[0], handleNamespace()) configWriter, err := setupConfigdumpEnvoyConfigWriter(podName, ns, c.OutOrStdout()) if err != nil { return err } return configWriter.PrintBootstrapDump() }, } clusterName, status string endpointConfigCmd = &cobra.Command{ Use: "endpoint <pod-name[.namespace]>", Short: "Retrieves endpoint configuration for the Envoy in the specified pod", Long: `Retrieve information about endpoint configuration for the Envoy instance in the specified pod.`, Example: ` # Retrieve full endpoint configuration for a given pod from Envoy. istioctl proxy-config endpoint <pod-name[.namespace]> # Retrieve endpoint summary for endpoint with port 9080. istioctl proxy-config endpoint <pod-name[.namespace]> --port 9080 # Retrieve full endpoint with a address (172.17.0.2). istioctl proxy-config endpoint <pod-name[.namespace]> --address 172.17.0.2 -o json # Retrieve full endpoint with a cluster name (outbound|9411||zipkin.istio-system.svc.cluster.local). istioctl proxy-config endpoint <pod-name[.namespace]> --cluster "outbound|9411||zipkin.istio-system.svc.cluster.local" -o json # Retrieve full endpoint with the status (healthy). istioctl proxy-config endpoint <pod-name[.namespace]> --status healthy -ojson `, Aliases: []string{"endpoints", "ep"}, Args: cobra.ExactArgs(1), RunE: func(c *cobra.Command, args []string) error { podName, ns := inferPodInfo(args[0], handleNamespace()) configWriter, err := setupClustersEnvoyConfigWriter(podName, ns, c.OutOrStdout()) if err != nil { return err } filter := clusters.EndpointFilter{ Address: address, Port: uint32(port), Cluster: clusterName, Status: status, } switch outputFormat { case summaryOutput: return configWriter.PrintEndpointsSummary(filter) case jsonOutput: return configWriter.PrintEndpoints(filter) default: return fmt.Errorf("output format %q not supported", outputFormat) } }, } ) func handleNamespace() string { ns := namespace if ns == v1.NamespaceAll { ns = defaultNamespace } return ns } func setupConfigdumpEnvoyConfigWriter(podName, podNamespace string, out io.Writer) (*configdump.ConfigWriter, error) { kubeClient, err := clientExecFactory(kubeconfig, configContext) if err != nil { return nil, fmt.Errorf("failed to create k8s client: %v", err) } path := "config_dump" debug, err := kubeClient.EnvoyDo(podName, podNamespace, "GET", path, nil) if err != nil { return nil, fmt.Errorf("failed to execute command on envoy: %v", err) } cw := &configdump.ConfigWriter{Stdout: out} err = cw.Prime(debug) if err != nil { return nil, err } return cw, nil } // TODO(fisherxu): migrate this to config dump when implemented in Envoy // Issue to track -> https://github.com/envoyproxy/envoy/issues/3362 func setupClustersEnvoyConfigWriter(podName, podNamespace string, out io.Writer) (*clusters.ConfigWriter, error) { kubeClient, err := clientExecFactory(kubeconfig, configContext) if err != nil { return nil, fmt.Errorf("failed to create k8s client: %v", err) } path := "clusters?format=json" debug, err := kubeClient.EnvoyDo(podName, podNamespace, "GET", path, nil) if err != nil { return nil, fmt.Errorf("failed to execute command on envoy: %v", err) } cw := &clusters.ConfigWriter{Stdout: out} err = cw.Prime(debug) if err != nil { return nil, err } return cw, nil } func init() { rootCmd.AddCommand(configCmd) configCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|short") clusterConfigCmd.PersistentFlags().StringVar(&fqdn, "fqdn", "", "Filter clusters by substring of Service FQDN field") clusterConfigCmd.PersistentFlags().StringVar(&direction, "direction", "", "Filter clusters by Direction field") clusterConfigCmd.PersistentFlags().StringVar(&subset, "subset", "", "Filter clusters by substring of Subset field") clusterConfigCmd.PersistentFlags().IntVar(&port, "port", 0, "Filter clusters by Port field") listenerConfigCmd.PersistentFlags().StringVar(&address, "address", "", "Filter listeners by address field") listenerConfigCmd.PersistentFlags().StringVar(&listenerType, "type", "", "Filter listeners by type field") listenerConfigCmd.PersistentFlags().IntVar(&port, "port", 0, "Filter listeners by Port field") routeConfigCmd.PersistentFlags().StringVar(&routeName, "name", "", "Filter listeners by route name field") endpointConfigCmd.PersistentFlags().StringVar(&address, "address", "", "Filter endpoints by address field") endpointConfigCmd.PersistentFlags().IntVar(&port, "port", 0, "Filter endpoints by Port field") endpointConfigCmd.PersistentFlags().StringVar(&clusterName, "cluster", "", "Filter endpoints by cluster name field") endpointConfigCmd.PersistentFlags().StringVar(&status, "status", "", "Filter endpoints by status field") configCmd.AddCommand(clusterConfigCmd, listenerConfigCmd, routeConfigCmd, bootstrapConfigCmd, endpointConfigCmd) }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "github.com/spf13/cobra" "istio.io/istio/istioctl/pkg/kubernetes" "istio.io/istio/istioctl/pkg/writer/compare" "istio.io/istio/istioctl/pkg/writer/pilot" ) var ( statusCmd = &cobra.Command{ Use: "proxy-status [<pod-name[.namespace]>]", Short: "Retrieves the synchronization status of each Envoy in the mesh [kube only]", Long: ` Retrieves last sent and last acknowledged xDS sync from Pilot to each Envoy in the mesh `, Example: `# Retrieve sync status for all Envoys in a mesh istioctl proxy-status # Retrieve sync diff for a single Envoy and Pilot istioctl proxy-status istio-egressgateway-59585c5b9c-ndc59.istio-system `, Aliases: []string{"ps"}, RunE: func(c *cobra.Command, args []string) error { kubeClient, err := clientExecFactory(kubeconfig, configContext) if err != nil { return err } if len(args) > 0 { podName, ns := inferPodInfo(args[0], handleNamespace()) path := fmt.Sprintf("config_dump") envoyDump, err := kubeClient.EnvoyDo(podName, ns, "GET", path, nil) if err != nil { return err } path = fmt.Sprintf("/debug/config_dump?proxyID=%s.%s", podName, ns) pilotDumps, err := kubeClient.AllPilotsDiscoveryDo(istioNamespace, "GET", path, nil) if err != nil { return err } c, err := compare.NewComparator(c.OutOrStdout(), pilotDumps, envoyDump) if err != nil { return err } return c.Diff() } statuses, err := kubeClient.AllPilotsDiscoveryDo(istioNamespace, "GET", "/debug/syncz", nil) if err != nil { return err } sw := pilot.StatusWriter{Writer: c.OutOrStdout()} return sw.PrintAll(statuses) }, } ) func init() { rootCmd.AddCommand(statusCmd) } func newExecClient(kubeconfig, configContext string) (kubernetes.ExecClient, error) { return kubernetes.NewClient(kubeconfig, configContext) }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "github.com/spf13/cobra" rbacproto "istio.io/api/rbac/v1alpha1" "istio.io/istio/istioctl/pkg/rbac" "istio.io/istio/pilot/pkg/model" ) // can allows user to query Istio RBAC effect for a specific request. func can() *cobra.Command { subject := rbac.SubjectArgs{} action := rbac.ActionArgs{} cmd := &cobra.Command{ Use: "can METHOD SERVICE PATH", Short: "Query Istio RBAC policy effect for a specific request", Long: ` This command lets you query whether a specific request will be allowed or denied under current Istio RBAC policies. It constructs a fake request with the custom subject and action specified in the command line to check if your Istio RBAC policies are working as expected. Note the fake request is only used locally to evaluate the effect of the Istio RBAC policies, no actual request will be issued. METHOD is the HTTP method being taken, like GET, POST, etc. SERVICE is the short service name the action is being taken on. PATH is the HTTP path within the service.`, Example: `# Query if user "cluster.local/ns/default/sa/productpage" is allowed to GET /v1/health of service rating. istioctl experimental rbac can -u cluster.local/ns/default/sa/productpage GET rating /v1/health # Query if namespace foo is allowed to POST to /data of service rating with label version=dev. istioctl experimental rbac can -s source.namespace=foo POST rating /data -a destination.labels[version]=dev`, Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { action.Method = args[0] action.Service = args[1] action.Path = args[2] if namespace == "" { action.Namespace = "default" } else { action.Namespace = namespace } rbacStore, err := newRbacStore() if err != nil { return fmt.Errorf("failed to create rbacStore: %v", err) } ret, err := rbacStore.CheckPermission(subject, action) if err != nil { return err } if ret { fmt.Println("allow") } else { fmt.Println("deny") } return nil }, } cmd.Flags().StringVarP(&subject.User, "user", "u", "", "[Subject] User name/ID that the subject represents.") cmd.Flags().StringArrayVarP(&subject.Properties, "subject-properties", "s", []string{}, "[Subject] Additional data about the subject. Specified as name1=value1,name2=value2,...") cmd.Flags().StringArrayVarP(&action.Properties, "action-properties", "a", []string{}, "[Action] Additional data about the action. Specified as name1=value1,name2=value2,...") return cmd } // Rbac provides a command named rbac that allows user to interact with Istio RBAC policies. func Rbac() *cobra.Command { cmd := &cobra.Command{ Use: "rbac", Short: "Interact with Istio RBAC policies", Long: ` A group of commands used to interact with Istio RBAC policies. For example, Query whether a specific request is allowed or denied under the current Istio RBAC policies.`, Example: `# Query if user test is allowed to GET /v1/health of service rating. istioctl experimental rbac can -u test GET rating /v1/health`, } cmd.AddCommand(can()) return cmd } func newRbacStore() (*rbac.ConfigStore, error) { client, err := newClient() if err != nil { return nil, err } roles, err := client.List(model.ServiceRole.Type, namespace) if err != nil { return nil, fmt.Errorf("failed to get ServiceRoles for namespace %v: %v", namespace, err) } bindings, err := client.List(model.ServiceRoleBinding.Type, namespace) if err != nil { return nil, fmt.Errorf("failed to get ServiceRoleBinding for namespace %v: %v", namespace, err) } rolesMap := make(rbac.RolesMapByNamespace) for _, role := range roles { proto := role.Spec.(*rbacproto.ServiceRole) err := rolesMap.AddServiceRole(role.Name, role.Namespace, proto) if err != nil { return nil, err } } for _, binding := range bindings { proto := binding.Spec.(*rbacproto.ServiceRoleBinding) err := rolesMap.AddServiceRoleBinding(binding.Name, binding.Namespace, proto) if err != nil { return nil, err } } return &rbac.ConfigStore{Roles: rolesMap}, nil }
// Copyright 2017 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "github.com/spf13/cobra" "istio.io/istio/pilot/pkg/serviceregistry/kube" "istio.io/istio/pkg/log" ) var ( registerCmd = &cobra.Command{ Use: "register <svcname> <ip> [name1:]port1 [name2:]port2 ...", Short: "Registers a service instance (e.g. VM) joining the mesh", Args: cobra.MinimumNArgs(3), RunE: func(c *cobra.Command, args []string) error { svcName := args[0] ip := args[1] portsListStr := args[2:] portsList := make([]kube.NamedPort, len(portsListStr)) for i := range portsListStr { p, err := kube.Str2NamedPort(portsListStr[i]) if err != nil { return err } portsList[i] = p } log.Infof("Registering for service '%s' ip '%s', ports list %v", svcName, ip, portsList) if svcAcctAnn != "" { annotations = append(annotations, fmt.Sprintf("%s=%s", kube.KubeServiceAccountsOnVMAnnotation, svcAcctAnn)) } log.Infof("%d labels (%v) and %d annotations (%v)", len(labels), labels, len(annotations), annotations) client, err := createInterface(kubeconfig) if err != nil { return err } ns, _ := handleNamespaces(namespace) return kube.RegisterEndpoint(client, ns, svcName, ip, portsList, labels, annotations) }, } labels []string annotations []string svcAcctAnn string ) func init() { rootCmd.AddCommand(registerCmd) registerCmd.PersistentFlags().StringSliceVarP(&labels, "labels", "l", nil, "List of labels to apply if creating a service/endpoint; e.g. -l env=prod,vers=2") registerCmd.PersistentFlags().StringSliceVarP(&annotations, "annotations", "a", nil, "List of string annotations to apply if creating a service/endpoint; e.g. -a foo=bar,test,x=y") registerCmd.PersistentFlags().StringVarP(&svcAcctAnn, "serviceaccount", "s", "default", "Service account to link to the service") }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package convert import ( "strings" "k8s.io/api/extensions/v1beta1" "istio.io/istio/pilot/pkg/config/kube/ingress" "istio.io/istio/pilot/pkg/model" ) // IstioIngresses converts K8s extensions/v1beta1 Ingresses with Istio rules to v1alpha3 gateway and virtual service func IstioIngresses(ingresses []*v1beta1.Ingress, domainSuffix string) ([]model.Config, error) { if len(ingresses) == 0 { return make([]model.Config, 0), nil } if len(domainSuffix) == 0 { domainSuffix = "cluster.local" } ingressByHost := map[string]*model.Config{} for _, ingrezz := range ingresses { ingress.ConvertIngressVirtualService(*ingrezz, domainSuffix, ingressByHost) } out := make([]model.Config, 0, len(ingressByHost)) for _, vs := range ingressByHost { // Ensure name is valid; ConvertIngressVirtualService will create a name that doesn't start with alphanumeric if strings.HasPrefix(vs.Name, "-") { vs.Name = "wild" + vs.Name } out = append(out, *vs) } return out, nil }
// Copyright 2018 Istio Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package install import ( "errors" "fmt" "io" "strings" "github.com/spf13/cobra" v1batch "k8s.io/api/batch/v1" "k8s.io/api/extensions/v1beta1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" "k8s.io/kubernetes/pkg/kubectl/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource" kube_meta "istio.io/istio/galley/pkg/metadata/kube" ) func verifyInstall(enableVerbose bool, istioNamespaceFlag *string, restClientGetter resource.RESTClientGetter, options resource.FilenameOptions, writer io.Writer) error { crdCount := 0 istioDeploymentCount := 0 if len(options.Filenames) == 0 { return errors.New("--filename must be set") } r := resource.NewBuilder(restClientGetter). Unstructured(). FilenameParam(false, &options). Flatten(). Do() if err := r.Err(); err != nil { return err } err := r.Visit(func(info *resource.Info, err error) error { content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) if err != nil { return err } un := &unstructured.Unstructured{Object: content} kind := un.GetKind() name := un.GetName() namespace := un.GetNamespace() kinds := findResourceInSpec(kind) if kinds == "" { kinds = strings.ToLower(kind) + "s" } if namespace == "" { namespace = "default" } switch kind { case "Deployment": deployment := &v1beta1.Deployment{} err = info.Client. Get(). Resource(kinds). Namespace(namespace). Name(name). VersionedParams(&meta_v1.GetOptions{}, scheme.ParameterCodec). Do(). Into(deployment) if err != nil { return err } err = getDeploymentStatus(deployment, name) if err != nil { return err } if namespace == *istioNamespaceFlag && strings.HasPrefix(name, "istio-") { istioDeploymentCount++ } case "Job": job := &v1batch.Job{} err = info.Client. Get(). Resource(kinds). Namespace(namespace). Name(name). VersionedParams(&meta_v1.GetOptions{}, scheme.ParameterCodec). Do(). Into(job) if err != nil { return err } for _, c := range job.Status.Conditions { switch c.Type { case v1batch.JobFailed: return fmt.Errorf("istio installation fails - the required Job %s failed", name) } } default: result := info.Client. Get(). Resource(kinds). Name(name). Do() if result.Error() != nil { result = info.Client. Get(). Resource(kinds). Namespace(namespace). Name(name). Do() if result.Error() != nil { return fmt.Errorf("istio installation fails or have not been completed - the required %s:%s is not ready due to: %v", kind, name, result.Error()) } } if kind == "CustomResourceDefinition" { crdCount++ } } if enableVerbose { fmt.Fprintf(writer, "%s: %s.%s checked successfully\n", kind, name, namespace) } return nil }) if err != nil { return err } fmt.Fprintf(writer, "Checked %v crds\n", crdCount) fmt.Fprintf(writer, "Checked %v Istio Deployments\n", istioDeploymentCount) fmt.Fprintf(writer, "Istio is installed successfully\n") return nil } // NewVerifyCommand creates a new command for verifying Istio Installation Status func NewVerifyCommand(istioNamespaceFlag *string) *cobra.Command { var ( kubeConfigFlags = &genericclioptions.ConfigFlags{ Context: strPtr(""), Namespace: strPtr(""), KubeConfig: strPtr(""), } filenames = []string{} fileNameFlags = &genericclioptions.FileNameFlags{ Filenames: &filenames, Recursive: boolPtr(true), Usage: "Istio YAML installation file.", } enableVerbose bool ) verifyInstallCmd := &cobra.Command{ Use: "verify-install", Short: "Verifies Istio Installation Status", Long: ` verify-install verifies Istio installation status against the installation file you specified when you installed Istio. It loops through all the installation resources defined in your installation file and reports whether all of them are in ready status. It will report failure when any of them are not ready. `, Example: ` istioctl verify-install -f istio-demo.yaml `, RunE: func(c *cobra.Command, _ []string) error { return verifyInstall(enableVerbose, istioNamespaceFlag, kubeConfigFlags, fileNameFlags.ToOptions(), c.OutOrStderr()) }, } flags := verifyInstallCmd.PersistentFlags() kubeConfigFlags.AddFlags(flags) fileNameFlags.AddFlags(flags) verifyInstallCmd.Flags().BoolVar(&enableVerbose, "enableVerbose", false, "Enable verbose output") return verifyInstallCmd } func strPtr(val string) *string { return &val } func boolPtr(val bool) *bool { return &val } func getDeploymentStatus(deployment *v1beta1.Deployment, name string) error { cond := getDeploymentCondition(deployment.Status, v1beta1.DeploymentProgressing) if cond != nil && cond.Reason == "ProgressDeadlineExceeded" { return fmt.Errorf("istio installation fails or have not been completed"+ " - deployment %q exceeded its progress deadline", name) } if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas { return fmt.Errorf("istio installation fails or have not been completed"+ " - waiting for deployment %q rollout to finish: %d out of %d new replicas have been updated", name, deployment.Status.UpdatedReplicas, *deployment.Spec.Replicas) } if deployment.Status.Replicas > deployment.Status.UpdatedReplicas { return fmt.Errorf("istio installation fails or have not been completed"+ " - waiting for deployment %q rollout to finish: %d old replicas are pending termination", name, deployment.Status.Replicas-deployment.Status.UpdatedReplicas) } if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas { return fmt.Errorf("istio installation fails or have not been completed"+ " - waiting for deployment %q rollout to finish: %d of %d updated replicas are available", name, deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas) } return nil } func getDeploymentCondition(status v1beta1.DeploymentStatus, condType v1beta1.DeploymentConditionType) *v1beta1.DeploymentCondition { for i := range status.Conditions { c := status.Conditions[i] if c.Type == condType { return &c } } return nil } func findResourceInSpec(kind string) string { for _, spec := range kube_meta.Types.All() { if spec.Kind == kind { return spec.Plural } } return "" }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubernetes import ( "bytes" "encoding/json" "errors" "fmt" "strings" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" "istio.io/istio/pkg/kube" "istio.io/istio/pkg/version" ) var ( proxyContainer = "istio-proxy" discoveryContainer = "discovery" pilotDiscoveryPath = "/usr/local/bin/pilot-discovery" pilotAgentPath = "/usr/local/bin/pilot-agent" ) // Client is a helper wrapper around the Kube RESTClient for istioctl -> Pilot/Envoy/Mesh related things type Client struct { Config *rest.Config *rest.RESTClient } // ExecClient is an interface for remote execution type ExecClient interface { EnvoyDo(podName, podNamespace, method, path string, body []byte) ([]byte, error) AllPilotsDiscoveryDo(pilotNamespace, method, path string, body []byte) (map[string][]byte, error) GetIstioVersions(namespace string) (*version.MeshInfo, error) } // NewClient is the constructor for the client wrapper func NewClient(kubeconfig, configContext string) (*Client, error) { config, err := defaultRestConfig(kubeconfig, configContext) if err != nil { return nil, err } restClient, err := rest.RESTClientFor(config) if err != nil { return nil, err } return &Client{config, restClient}, nil } func defaultRestConfig(kubeconfig, configContext string) (*rest.Config, error) { config, err := kube.BuildClientConfig(kubeconfig, configContext) if err != nil { return nil, err } config.APIPath = "/api" config.GroupVersion = &v1.SchemeGroupVersion config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} return config, nil } // PodExec takes a command and the pod data to run the command in the specified pod func (client *Client) PodExec(podName, podNamespace, container string, command []string) (*bytes.Buffer, *bytes.Buffer, error) { req := client.Post(). Resource("pods"). Name(podName). Namespace(podNamespace). SubResource("exec"). Param("container", container). VersionedParams(&v1.PodExecOptions{ Container: container, Command: command, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(client.Config, "POST", req.URL()) if err != nil { return nil, nil, err } var stdout, stderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdin: nil, Stdout: &stdout, Stderr: &stderr, Tty: false, }) return &stdout, &stderr, err } // AllPilotsDiscoveryDo makes an http request to each Pilot discovery instance func (client *Client) AllPilotsDiscoveryDo(pilotNamespace, method, path string, body []byte) (map[string][]byte, error) { pilots, err := client.GetIstioPods(pilotNamespace, map[string]string{ "labelSelector": "istio=pilot", "fieldSelector": "status.phase=Running", }) if err != nil { return nil, err } if len(pilots) == 0 { return nil, errors.New("unable to find any Pilot instances") } cmd := []string{"sh", "-c", fmt.Sprintf("GODEBUG= %s request %s %s %s", pilotDiscoveryPath, method, path, string(body))} result := map[string][]byte{} for _, pilot := range pilots { res, err := client.ExtractExecResult(pilot.Name, pilot.Namespace, discoveryContainer, cmd) if err != nil { return nil, err } if len(res) > 0 { result[pilot.Name] = res } } return result, err } // PilotDiscoveryDo makes an http request to a single Pilot discovery instance func (client *Client) PilotDiscoveryDo(pilotNamespace, method, path string, body []byte) ([]byte, error) { pilots, err := client.GetIstioPods(pilotNamespace, map[string]string{ "labelSelector": "istio=pilot", "fieldSelector": "status.phase=Running", }) if err != nil { return nil, err } if len(pilots) == 0 { return nil, errors.New("unable to find any Pilot instances") } cmd := []string{"sh", "-c", fmt.Sprintf("GODEBUG= %s request %s %s %s", pilotDiscoveryPath, method, path, string(body))} return client.ExtractExecResult(pilots[0].Name, pilots[0].Namespace, discoveryContainer, cmd) } // EnvoyDo makes an http request to the Envoy in the specified pod func (client *Client) EnvoyDo(podName, podNamespace, method, path string, body []byte) ([]byte, error) { container, err := client.GetPilotAgentContainer(podName, podNamespace) if err != nil { return nil, fmt.Errorf("unable to retrieve proxy container name: %v", err) } cmd := []string{pilotAgentPath, "request", method, path, string(body)} return client.ExtractExecResult(podName, podNamespace, container, cmd) } // ExtractExecResult wraps PodExec and return the execution result and error if has any. func (client *Client) ExtractExecResult(podName, podNamespace, container string, cmd []string) ([]byte, error) { stdout, stderr, err := client.PodExec(podName, podNamespace, container, cmd) if err != nil { return nil, fmt.Errorf("error execing into %v/%v %v container: %v", podName, podNamespace, container, err) } if stderr.String() != "" { fmt.Printf("Warning! error execing into %v/%v %v container: %v\n", podName, podNamespace, container, stderr.String()) } return stdout.Bytes(), nil } // GetIstioPods retrieves the pod objects for Istio deployments func (client *Client) GetIstioPods(namespace string, params map[string]string) ([]v1.Pod, error) { req := client.Get(). Resource("pods"). Namespace(namespace) for k, v := range params { req.Param(k, v) } res := req.Do() if res.Error() != nil { return nil, fmt.Errorf("unable to retrieve Pods: %v", res.Error()) } list := &v1.PodList{} if err := res.Into(list); err != nil { return nil, fmt.Errorf("unable to parse PodList: %v", res.Error()) } return list.Items, nil } // GetPilotAgentContainer retrieves the pilot-agent container name for the specified pod func (client *Client) GetPilotAgentContainer(podName, podNamespace string) (string, error) { req := client.Get(). Resource("pods"). Namespace(podNamespace). Name(podName) res := req.Do() if res.Error() != nil { return "", fmt.Errorf("unable to retrieve Pod: %v", res.Error()) } pod := &v1.Pod{} if err := res.Into(pod); err != nil { return "", fmt.Errorf("unable to parse Pod: %v", res.Error()) } for _, c := range pod.Spec.Containers { switch c.Name { case "egressgateway", "ingress", "ingressgateway": return c.Name, nil } } return proxyContainer, nil } type podDetail struct { binary string container string } // GetIstioVersions gets the version for each Istio component func (client *Client) GetIstioVersions(namespace string) (*version.MeshInfo, error) { pods, err := client.GetIstioPods(namespace, map[string]string{ "labelSelector": "istio", "fieldSelector": "status.phase=Running", }) if err != nil { return nil, err } if len(pods) == 0 { return nil, errors.New("unable to find any Istio pod in namespace " + namespace) } labelToPodDetail := map[string]podDetail{ "pilot": {"/usr/local/bin/pilot-discovery", "discovery"}, "citadel": {"/usr/local/bin/istio_ca", "citadel"}, "egressgateway": {"/usr/local/bin/pilot-agent", "istio-proxy"}, "galley": {"/usr/local/bin/galley", "galley"}, "ingressgateway": {"/usr/local/bin/pilot-agent", "istio-proxy"}, "telemetry": {"/usr/local/bin/mixs", "mixer"}, "policy": {"/usr/local/bin/mixs", "mixer"}, "sidecar-injector": {"/usr/local/bin/sidecar-injector", "sidecar-injector-webhook"}, } res := version.MeshInfo{} for _, pod := range pods { component := pod.Labels["istio"] // Special cases switch component { case "statsd-prom-bridge": continue case "mixer": component = pod.Labels["istio-mixer-type"] } server := version.ServerInfo{Component: component} if detail, ok := labelToPodDetail[component]; ok { cmd := []string{detail.binary, "version"} cmdJSON := append(cmd, "-o", "json") var info version.BuildInfo var v version.Version stdout, stderr, err := client.PodExec(pod.Name, pod.Namespace, detail.container, cmdJSON) if err != nil { return nil, fmt.Errorf("error execing into %v %v container: %v", pod.Name, detail.container, err) } // At first try parsing stdout err = json.Unmarshal(stdout.Bytes(), &v) if err == nil && v.ClientVersion.Version != "" { info = *v.ClientVersion } else { // If stdout fails, try the old behavior if strings.HasPrefix(stderr.String(), "Error: unknown shorthand flag") { stdout, err := client.ExtractExecResult(pod.Name, pod.Namespace, detail.container, cmd) if err != nil { return nil, fmt.Errorf("error execing into %v %v container: %v", pod.Name, detail.container, err) } info, err = version.NewBuildInfoFromOldString(string(stdout)) if err != nil { return nil, fmt.Errorf("error converting server info from JSON: %v", err) } } else { return nil, fmt.Errorf("error execing into %v %v container: %v", pod.Name, detail.container, stderr.String()) } } server.Info = info } res = append(res, server) } return &res, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package rbac import ( "fmt" "strings" rbacproto "istio.io/api/rbac/v1alpha1" "istio.io/istio/pkg/log" ) const ( // ServiceRoleKind defines the config kind name of ServiceRole. serviceRoleKind = "ServiceRole" ) // SubjectArgs contains information about the subject of a request. type SubjectArgs struct { User string Groups string Properties []string } // ActionArgs contains information about the detail of a request. type ActionArgs struct { Namespace string Service string Method string Path string Properties []string } // RoleInfo contains information about a ServiceRole and associated ServiceRoleBindings. type RoleInfo struct { // ServiceRole proto definition Info *rbacproto.ServiceRole // A set of ServiceRoleBindings that refer to this role. Bindings map[string]*rbacproto.ServiceRoleBinding } // RolesByName maps role name to role info type RolesByName map[string]*RoleInfo // RolesMapByNamespace maps namespace to a set of Roles in the namespace type RolesMapByNamespace map[string]RolesByName // ConfigStore contains all ServiceRole and ServiceRoleBinding information. // ConfigStore implements authorizer interface. type ConfigStore struct { // All the Roles organized per namespace. Roles RolesMapByNamespace } // Create a RoleInfo object. func newRoleInfo(spec *rbacproto.ServiceRole) *RoleInfo { return &RoleInfo{ Info: spec, } } // AddServiceRole adds a new ServiceRole to RolesMapByNamespace with the specified name and namespace. // Return nil if added successfully, otherwise return an error. func (rs *RolesMapByNamespace) AddServiceRole(name, namespace string, proto *rbacproto.ServiceRole) error { if rs == nil { return nil } rolesByName := (*rs)[namespace] if rolesByName == nil { rolesByName = make(RolesByName) (*rs)[namespace] = rolesByName } if _, present := rolesByName[name]; present { return fmt.Errorf("duplicate ServiceRole: %v", name) } rolesByName[name] = newRoleInfo(proto) return nil } // AddServiceRoleBinding adds a new ServiceRoleBinding to RolesMapByNamespace with the specified // name and namespace. Return nil if added successfully, otherwise return an error. func (rs *RolesMapByNamespace) AddServiceRoleBinding(name, namespace string, proto *rbacproto.ServiceRoleBinding) error { if rs == nil { return nil } if proto.RoleRef.Kind != serviceRoleKind { return fmt.Errorf("roleBinding %s has role kind %s, expected %s", name, proto.RoleRef.Kind, serviceRoleKind) } rolesByName := (*rs)[namespace] if rolesByName == nil { return fmt.Errorf("roleBinding %s is in a namespace (%s) that no valid role is defined", name, namespace) } refName := proto.RoleRef.Name roleInfo := rolesByName[refName] if refName == "" { return fmt.Errorf("roleBinding %s does not refer to a valid role name", refName) } if roleInfo == nil { return fmt.Errorf("roleBinding %s is bound to a role that does not exist %s", name, refName) } if _, present := roleInfo.Bindings[name]; present { return fmt.Errorf("duplicate RoleBinding: %v", name) } if roleInfo.Bindings == nil { roleInfo.Bindings = make(map[string]*rbacproto.ServiceRoleBinding) } roleInfo.Bindings[name] = proto return nil } func convertProperties(arguments []string) map[string]string { properties := map[string]string{} for _, arg := range arguments { // Use the part before the first = as key and the remaining part as a string value, this is // the only supported format for now. split := strings.SplitN(arg, "=", 2) if len(split) != 2 { log.Debugf("invalid property %v, the format should be: key=value", arg) return nil } value, present := properties[split[0]] if present { log.Debugf("duplicate property %v, previous value %v", arg, value) return nil } properties[split[0]] = split[1] } return properties } // CheckPermission checks permission for a given subject and action. // TODO(yangminzhu): Refactor and support checking RbacConfig. func (rs *ConfigStore) CheckPermission(subject SubjectArgs, action ActionArgs) (bool, error) { if action.Namespace == "" { return false, fmt.Errorf("missing namespace") } if action.Service == "" { return false, fmt.Errorf("missing service") } if action.Path == "" { return false, fmt.Errorf("missing path") } if action.Method == "" { return false, fmt.Errorf("missing method") } rn := rs.Roles[action.Namespace] if rn == nil { return false, nil } for rolename, roleInfo := range rn { eligibleRole := false log.Debugf("Checking role: %s", rolename) rules := roleInfo.Info.GetRules() for _, rule := range rules { if matchRule(action.Service, action.Path, action.Method, convertProperties(action.Properties), rule) { eligibleRole = true log.Debugf("rule matched") break } } if !eligibleRole { log.Debugf("role %s is not eligible", rolename) continue } log.Debugf("role %s is eligible", rolename) bindings := roleInfo.Bindings for _, binding := range bindings { log.Debugf("Checking binding %v", binding) for _, sub := range binding.GetSubjects() { foundMatch := false if sub.GetUser() != "" { if sub.GetUser() == "*" || sub.GetUser() == subject.User { foundMatch = true } else { // Found a mismatch, try next sub. continue } } subProp := sub.GetProperties() if len(subProp) != 0 { if checkSubject(convertProperties(subject.Properties), subProp) { foundMatch = true } else { // Found a mismatch, try next sub. continue } } if foundMatch { log.Debugf("binding matched") return true, nil } } } } return false, nil } // Helper function to check whether or not a request matches a rule in a ServiceRole specification. func matchRule(serviceName string, path string, method string, extraAct map[string]string, rule *rbacproto.AccessRule) bool { services := rule.GetServices() paths := rule.GetPaths() methods := rule.GetMethods() constraints := rule.GetConstraints() log.Debugf("Checking rule: services %v, path %v, method %v, constraints %v", services, paths, methods, constraints) if stringMatch(serviceName, services) && (paths == nil || stringMatch(path, paths)) && stringMatch(method, methods) && checkConstraints(extraAct, constraints) { return true } return false } // Helper function to check if a string is in a list. // We support four types of string matches: // 1. Exact match. // 2. Wild character match. "*" matches any string. // 3. Prefix match. For example, "book*" matches "bookstore", "bookshop", etc. // 4. Suffix match. For example, "*/review" matches "/bookstore/review", "/products/review", etc. func stringMatch(a string, list []string) bool { for _, s := range list { if a == s || s == "*" || prefixMatch(a, s) || suffixMatch(a, s) { return true } } return false } // Helper function to check if string "a" prefix matches "pattern". func prefixMatch(a string, pattern string) bool { if !strings.HasSuffix(pattern, "*") { return false } pattern = strings.TrimSuffix(pattern, "*") return strings.HasPrefix(a, pattern) } // Helper function to check if string "a" prefix matches "pattern". func suffixMatch(a string, pattern string) bool { if !strings.HasPrefix(pattern, "*") { return false } pattern = strings.TrimPrefix(pattern, "*") return strings.HasSuffix(a, pattern) } // Helper function to check if a given string value is in a list of strings. func valueInList(value interface{}, list []string) bool { if str, ok := value.(string); ok { return stringMatch(str, list) } return false } // Check if all constraints in a rule can be satisfied by the properties from the request. func checkConstraints(properties map[string]string, constraints []*rbacproto.AccessRule_Constraint) bool { for _, constraint := range constraints { foundMatch := false for pn, pv := range properties { if pn == constraint.GetKey() && valueInList(pv, constraint.GetValues()) { foundMatch = true break } } if !foundMatch { // constraints of the rule is not satisfied, skip the rule return false } } return true } // Check if all properties defined for the subject are satisfied by the properties from the request. func checkSubject(properties map[string]string, subject map[string]string) bool { for sn, sv := range subject { if properties[sn] != sv { return false } } return true }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package clusters import ( "bytes" adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha" "github.com/gogo/protobuf/jsonpb" ) // Wrapper is a wrapper around the Envoy Clusters // It has extra helper functions for handling any/struct/marshal protobuf pain type Wrapper struct { *adminapi.Clusters } // MarshalJSON is a custom marshaller to handle protobuf pain func (w *Wrapper) MarshalJSON() ([]byte, error) { buffer := &bytes.Buffer{} jsonm := &jsonpb.Marshaler{} err := jsonm.Marshal(buffer, w) if err != nil { return nil, err } return buffer.Bytes(), nil } // UnmarshalJSON is a custom unmarshaller to handle protobuf pain func (w *Wrapper) UnmarshalJSON(b []byte) error { buf := bytes.NewBuffer(b) jsonum := &jsonpb.Unmarshaler{} cd := &adminapi.Clusters{} err := jsonum.Unmarshal(buf, cd) *w = Wrapper{cd} return err }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "fmt" adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha" proto "github.com/gogo/protobuf/types" ) // GetBootstrapConfigDump retrieves the bootstrap config dump from the ConfigDump func (w *Wrapper) GetBootstrapConfigDump() (*adminapi.BootstrapConfigDump, error) { // The bootstrap dump is the first one in the list. // See https://www.envoyproxy.io/docs/envoy/latest/api-v2/admin/v2alpha/config_dump.proto if len(w.Configs) < 1 { return nil, fmt.Errorf("config dump has no bootstrap dump") } bootstrapDumpAny := w.Configs[0] bootstrapDump := &adminapi.BootstrapConfigDump{} err := proto.UnmarshalAny(&bootstrapDumpAny, bootstrapDump) if err != nil { return nil, err } return bootstrapDump, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "fmt" "sort" adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha" proto "github.com/gogo/protobuf/types" ) // GetDynamicClusterDump retrieves a cluster dump with just dynamic active clusters in it func (w *Wrapper) GetDynamicClusterDump(stripVersions bool) (*adminapi.ClustersConfigDump, error) { clusterDump, err := w.GetClusterConfigDump() if err != nil { return nil, err } dac := clusterDump.GetDynamicActiveClusters() sort.Slice(dac, func(i, j int) bool { return dac[i].Cluster.Name < dac[j].Cluster.Name }) if stripVersions { for i := range dac { dac[i].VersionInfo = "" dac[i].LastUpdated = nil } } return &adminapi.ClustersConfigDump{DynamicActiveClusters: dac}, nil } // GetClusterConfigDump retrieves the cluster config dump from the ConfigDump func (w *Wrapper) GetClusterConfigDump() (*adminapi.ClustersConfigDump, error) { // The cluster dump is the second one in the list. // See https://www.envoyproxy.io/docs/envoy/latest/api-v2/admin/v2alpha/config_dump.proto if len(w.Configs) < 2 { return nil, fmt.Errorf("config dump has no cluster dump") } clusterDumpAny := w.Configs[1] clusterDump := &adminapi.ClustersConfigDump{} err := proto.UnmarshalAny(&clusterDumpAny, clusterDump) if err != nil { return nil, err } return clusterDump, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "fmt" "sort" adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha" proto "github.com/gogo/protobuf/types" ) // GetDynamicListenerDump retrieves a listener dump with just dynamic active listeners in it func (w *Wrapper) GetDynamicListenerDump(stripVersions bool) (*adminapi.ListenersConfigDump, error) { listenerDump, err := w.GetListenerConfigDump() if err != nil { return nil, err } dal := listenerDump.GetDynamicActiveListeners() sort.Slice(dal, func(i, j int) bool { return dal[i].Listener.Name < dal[j].Listener.Name }) if stripVersions { for i := range dal { dal[i].VersionInfo = "" dal[i].LastUpdated = nil } } return &adminapi.ListenersConfigDump{DynamicActiveListeners: dal}, nil } // GetListenerConfigDump retrieves the listener config dump from the ConfigDump func (w *Wrapper) GetListenerConfigDump() (*adminapi.ListenersConfigDump, error) { // The listener dump is the third one in the list. // See https://www.envoyproxy.io/docs/envoy/latest/api-v2/admin/v2alpha/config_dump.proto if len(w.Configs) < 3 { return nil, fmt.Errorf("config dump has no listener dump") } listenerDumpAny := w.Configs[2] listenerDump := &adminapi.ListenersConfigDump{} err := proto.UnmarshalAny(&listenerDumpAny, listenerDump) if err != nil { return nil, err } return listenerDump, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "fmt" "sort" "time" adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha" proto "github.com/gogo/protobuf/types" ) // GetLastUpdatedDynamicRouteTime retrieves the LastUpdated timestamp of the // most recently updated DynamicRouteConfig func (w *Wrapper) GetLastUpdatedDynamicRouteTime() (*time.Time, error) { routeDump, err := w.GetRouteConfigDump() if err != nil { return nil, err } drc := routeDump.GetDynamicRouteConfigs() lastUpdated := time.Unix(0, 0) // get the oldest possible timestamp for i := range drc { if drc[i].LastUpdated != nil { if drLastUpdated, err := proto.TimestampFromProto(drc[i].LastUpdated); err != nil { return nil, err } else if drLastUpdated.After(lastUpdated) { lastUpdated = drLastUpdated } } } if lastUpdated.After(time.Unix(0, 0)) { // if a timestamp was obtained from a drc return &lastUpdated, nil } return nil, nil } // GetDynamicRouteDump retrieves a route dump with just dynamic active routes in it func (w *Wrapper) GetDynamicRouteDump(stripVersions bool) (*adminapi.RoutesConfigDump, error) { routeDump, err := w.GetRouteConfigDump() if err != nil { return nil, err } drc := routeDump.GetDynamicRouteConfigs() sort.Slice(drc, func(i, j int) bool { return drc[i].RouteConfig.Name < drc[j].RouteConfig.Name }) if stripVersions { for i := range drc { drc[i].VersionInfo = "" drc[i].LastUpdated = nil } } return &adminapi.RoutesConfigDump{DynamicRouteConfigs: drc}, nil } // GetRouteConfigDump retrieves the route config dump from the ConfigDump func (w *Wrapper) GetRouteConfigDump() (*adminapi.RoutesConfigDump, error) { // The route dump is the fourth one in the list. // See https://www.envoyproxy.io/docs/envoy/latest/api-v2/admin/v2alpha/config_dump.proto if len(w.Configs) < 4 { return nil, fmt.Errorf("config dump has no route dump") } routeDumpAny := w.Configs[3] routeDump := &adminapi.RoutesConfigDump{} err := proto.UnmarshalAny(&routeDumpAny, routeDump) if err != nil { return nil, err } return routeDump, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "bytes" adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha" "github.com/gogo/protobuf/jsonpb" ) // Wrapper is a wrapper around the Envoy ConfigDump // It has extra helper functions for handling any/struct/marshal protobuf pain type Wrapper struct { *adminapi.ConfigDump } // MarshalJSON is a custom marshaller to handle protobuf pain func (w *Wrapper) MarshalJSON() ([]byte, error) { buffer := &bytes.Buffer{} err := (&jsonpb.Marshaler{}).Marshal(buffer, w) if err != nil { return nil, err } return buffer.Bytes(), nil } // UnmarshalJSON is a custom unmarshaller to handle protobuf pain func (w *Wrapper) UnmarshalJSON(b []byte) error { cd := &adminapi.ConfigDump{} err := (&jsonpb.Unmarshaler{AllowUnknownFields: true}).Unmarshal(bytes.NewReader(b), cd) *w = Wrapper{cd} return err }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package proto import ( "bytes" "github.com/gogo/protobuf/jsonpb" "github.com/gogo/protobuf/proto" ) // MessageSlice allows us to marshal slices of protobuf messages like clusters/listeners/routes/endpoints correctly type MessageSlice []proto.Message // MarshalJSON handles marshaling of slices of proto messages func (pSlice MessageSlice) MarshalJSON() ([]byte, error) { buffer := bytes.NewBufferString("[") sliceLength := len(pSlice) jsonm := &jsonpb.Marshaler{} for index, msg := range pSlice { if err := jsonm.Marshal(buffer, msg); err != nil { return nil, err } if index < sliceLength-1 { buffer.WriteString(",") } } buffer.WriteString("]") return buffer.Bytes(), nil }
// Copyright 2018 Istio Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package validate import ( "errors" "fmt" multierror "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/kubectl/genericclioptions" "k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource" "istio.io/istio/pilot/pkg/config/kube/crd" "istio.io/istio/pilot/pkg/model" ) // TODO use k8s.io/cli-runtime when we switch to v1.12 k8s dependency // k8s.io/cli-runtime was created for k8s v.12. Prior to that release, // the genericclioptions packages are organized under kubectl. var ( // Expect YAML to only include these top-level fields validFields = map[string]bool{ "apiVersion": true, "kind": true, "metadata": true, "spec": true, "status": true, } ) /* TODO(https://github.com/istio/istio/issues/4887) Reusing the existing mixer validation code pulls in all of the mixer adapter packages into istioctl. Not only is this not ideal (see issue), but it also breaks the istioctl build on windows as some mixer adapters use linux specific packges (e.g. syslog). func createMixerValidator() store.BackendValidator { info := generatedTmplRepo.SupportedTmplInfo templates := make(map[string]*template.Info, len(info)) for k := range info { t := info[k] templates[k] = &t } adapters := config.AdapterInfoMap(adapter.Inventory(), template.NewRepository(info).SupportsTemplate) return store.NewValidator(nil, runtimeConfig.KindMap(adapters, templates)) } var mixerValidator = createMixerValidator() type validateArgs struct { filenames []string // TODO validateObjectStream namespace/object? } func (args validateArgs) validate() error { var errs *multierror.Error if len(args.filenames) == 0 { errs = multierror.Append(errs, errors.New("no filenames set (see --filename/-f)")) } return errs.ErrorOrNil() } */ func validateResource(un *unstructured.Unstructured) error { schema, exists := model.IstioConfigTypes.GetByType(crd.CamelCaseToKebabCase(un.GetKind())) if exists { obj, err := crd.ConvertObjectFromUnstructured(schema, un, "") if err != nil { return fmt.Errorf("cannot parse proto message: %v", err) } return schema.Validate(obj.Name, obj.Namespace, obj.Spec) } return fmt.Errorf("%s.%s validation is not supported", un.GetKind(), un.GetAPIVersion()) /* TODO(https://github.com/istio/istio/issues/4887) ev := &store.BackendEvent{ Key: store.Key{ Name: un.GetName(), Namespace: un.GetNamespace(), Kind: un.GetKind(), }, Value: mixerCrd.ToBackEndResource(un), } return mixerValidator.Validate(ev) */ } var errMissingResource = errors.New(`error: you must specify resources by --filename. Example resource specifications include: '-f rsrc.yaml' '--filename=rsrc.json'`) func validateObjects(restClientGetter resource.RESTClientGetter, options resource.FilenameOptions) error { // resource.Builder{} validates most of the CLI flags consistent // with kubectl which is good. Unfortunately, it also assumes // resources can be specified as '<resource> <name>' which is // bad. We don't don't for file-based configuration validation. If // a filename is missing, resource.Builder{} prints a warning // referencing '<resource> <name>' which would be confusing to the // user. Avoid this confusion by checking for missing filenames // are ourselves for invoking the builder. if len(options.Filenames) == 0 { return errMissingResource } r := resource.NewBuilder(restClientGetter). Unstructured(). FilenameParam(false, &options). Flatten(). Local(). Do() if err := r.Err(); err != nil { return err } var errs error _ = r.Visit(func(info *resource.Info, err error) error { content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) if err != nil { errs = multierror.Append(errs, err) return nil } un := &unstructured.Unstructured{Object: content} for key := range content { if _, ok := validFields[key]; !ok { errs = multierror.Append(errs, fmt.Errorf("%s: Unknown field %q on %s resource %s namespace %q", info.Source, key, un.GetObjectKind().GroupVersionKind(), un.GetName(), un.GetNamespace())) } } if err := validateResource(un); err != nil { errs = multierror.Append(errs, fmt.Errorf("error validating resource (%v Name=%v Namespace=%v): %v", un.GetObjectKind().GroupVersionKind(), un.GetName(), un.GetNamespace(), err)) } return nil }) return errs } func strPtr(val string) *string { return &val } func boolPtr(val bool) *bool { return &val } // NewValidateCommand creates a new command for validating Istio k8s resources. func NewValidateCommand() *cobra.Command { var ( kubeConfigFlags = &genericclioptions.ConfigFlags{ Context: strPtr(""), Namespace: strPtr(""), KubeConfig: strPtr(""), } filenames = []string{} fileNameFlags = &genericclioptions.FileNameFlags{ Filenames: &filenames, Recursive: boolPtr(true), } ) c := &cobra.Command{ Use: "validate -f FILENAME [options]", Short: "Validate Istio policy and rules", Example: `istioctl validate -f bookinfo-gateway.yaml`, Args: cobra.NoArgs, RunE: func(c *cobra.Command, _ []string) error { return validateObjects(kubeConfigFlags, fileNameFlags.ToOptions()) }, } flags := c.PersistentFlags() kubeConfigFlags.AddFlags(flags) fileNameFlags.AddFlags(flags) return c }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package compare import ( "bytes" "fmt" "github.com/gogo/protobuf/jsonpb" "github.com/pmezard/go-difflib/difflib" ) // ClusterDiff prints a diff between Pilot and Envoy clusters to the passed writer func (c *Comparator) ClusterDiff() error { jsonm := &jsonpb.Marshaler{Indent: " "} envoyBytes, pilotBytes := &bytes.Buffer{}, &bytes.Buffer{} envoyClusterDump, err := c.envoy.GetDynamicClusterDump(true) if err != nil { envoyBytes.WriteString(err.Error()) } else if err := jsonm.Marshal(envoyBytes, envoyClusterDump); err != nil { return err } pilotClusterDump, err := c.pilot.GetDynamicClusterDump(true) if err != nil { pilotBytes.WriteString(err.Error()) } else if err := jsonm.Marshal(pilotBytes, pilotClusterDump); err != nil { return err } diff := difflib.UnifiedDiff{ FromFile: "Pilot Clusters", A: difflib.SplitLines(pilotBytes.String()), ToFile: "Envoy Clusters", B: difflib.SplitLines(envoyBytes.String()), Context: c.context, } text, err := difflib.GetUnifiedDiffString(diff) if err != nil { return err } if text != "" { fmt.Fprintln(c.w, text) } else { fmt.Fprintln(c.w, "Clusters Match") } return nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package compare import ( "encoding/json" "fmt" "io" "istio.io/istio/istioctl/pkg/util/configdump" ) // Comparator diffs between a config dump from Pilot and one from Envoy type Comparator struct { envoy, pilot *configdump.Wrapper w io.Writer context int location string } // NewComparator is a comparator constructor func NewComparator(w io.Writer, pilotResponses map[string][]byte, envoyResponse []byte) (*Comparator, error) { c := &Comparator{} for _, resp := range pilotResponses { pilotDump := &configdump.Wrapper{} err := json.Unmarshal(resp, pilotDump) if err != nil { continue } c.pilot = pilotDump break } if c.pilot == nil { return nil, fmt.Errorf("unable to find config dump in Pilot responses") } envoyDump := &configdump.Wrapper{} err := json.Unmarshal(envoyResponse, envoyDump) if err != nil { return nil, err } c.envoy = envoyDump c.w = w c.context = 7 c.location = "Local" // the time.Location for formatting time.Time instances return c, nil } // Diff prints a diff between Pilot and Envoy to the passed writer func (c *Comparator) Diff() error { if err := c.ClusterDiff(); err != nil { return err } if err := c.ListenerDiff(); err != nil { return err } return c.RouteDiff() }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package compare import ( "bytes" "fmt" "github.com/gogo/protobuf/jsonpb" "github.com/pmezard/go-difflib/difflib" ) // ListenerDiff prints a diff between Pilot and Envoy listeners to the passed writer func (c *Comparator) ListenerDiff() error { jsonm := &jsonpb.Marshaler{Indent: " "} envoyBytes, pilotBytes := &bytes.Buffer{}, &bytes.Buffer{} envoyListenerDump, err := c.envoy.GetDynamicListenerDump(true) if err != nil { envoyBytes.WriteString(err.Error()) } else if err := jsonm.Marshal(envoyBytes, envoyListenerDump); err != nil { return err } pilotListenerDump, err := c.pilot.GetDynamicListenerDump(true) if err != nil { pilotBytes.WriteString(err.Error()) } else if err := jsonm.Marshal(pilotBytes, pilotListenerDump); err != nil { return err } diff := difflib.UnifiedDiff{ FromFile: "Pilot Listeners", A: difflib.SplitLines(pilotBytes.String()), ToFile: "Envoy Listeners", B: difflib.SplitLines(envoyBytes.String()), Context: c.context, } text, err := difflib.GetUnifiedDiffString(diff) if err != nil { return err } if text != "" { fmt.Fprintln(c.w, text) } else { fmt.Fprintln(c.w, "Listeners Match") } return nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package compare import ( "bytes" "fmt" "time" "github.com/gogo/protobuf/jsonpb" "github.com/pmezard/go-difflib/difflib" ) // RouteDiff prints a diff between Pilot and Envoy routes to the passed writer func (c *Comparator) RouteDiff() error { jsonm := &jsonpb.Marshaler{Indent: " "} envoyBytes, pilotBytes := &bytes.Buffer{}, &bytes.Buffer{} envoyRouteDump, err := c.envoy.GetDynamicRouteDump(true) if err != nil { envoyBytes.WriteString(err.Error()) } else if err := jsonm.Marshal(envoyBytes, envoyRouteDump); err != nil { return err } pilotRouteDump, err := c.pilot.GetDynamicRouteDump(true) if err != nil { pilotBytes.WriteString(err.Error()) } else if err := jsonm.Marshal(pilotBytes, pilotRouteDump); err != nil { return err } diff := difflib.UnifiedDiff{ FromFile: "Pilot Routes", A: difflib.SplitLines(pilotBytes.String()), ToFile: "Envoy Routes", B: difflib.SplitLines(envoyBytes.String()), Context: c.context, } text, err := difflib.GetUnifiedDiffString(diff) if err != nil { return err } lastUpdatedStr := "" if lastUpdated, err := c.envoy.GetLastUpdatedDynamicRouteTime(); err != nil { return err } else if lastUpdated != nil { loc, err := time.LoadLocation(c.location) if err != nil { loc, _ = time.LoadLocation("UTC") } lastUpdatedStr = fmt.Sprintf(" (RDS last loaded at %s)", lastUpdated.In(loc).Format(time.RFC1123)) } if text != "" { fmt.Fprintln(c.w, fmt.Sprintf("Routes Don't Match%s", lastUpdatedStr)) fmt.Fprintln(c.w, text) } else { fmt.Fprintln(c.w, fmt.Sprintf("Routes Match%s", lastUpdatedStr)) } return nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package clusters import ( "encoding/json" "fmt" "io" "sort" "strconv" "strings" "text/tabwriter" adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha" "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" "istio.io/istio/istioctl/pkg/util/clusters" protio "istio.io/istio/istioctl/pkg/util/proto" ) // EndpointFilter is used to pass filter information into route based config writer print functions type EndpointFilter struct { Address string Port uint32 Cluster string Status string } // ConfigWriter is a writer for processing responses from the Envoy Admin config_dump endpoint type ConfigWriter struct { Stdout io.Writer clusters *clusters.Wrapper } // EndpointCluster is used to store the endpoint and cluster type EndpointCluster struct { address string port int cluster string status core.HealthStatus } // Prime loads the clusters output into the writer ready for printing func (c *ConfigWriter) Prime(b []byte) error { cd := clusters.Wrapper{} err := json.Unmarshal(b, &cd) if err != nil { return fmt.Errorf("error unmarshalling config dump response from Envoy: %v", err) } c.clusters = &cd return nil } func retrieveEndpointAddress(host *adminapi.HostStatus) string { return host.Address.GetSocketAddress().Address } func retrieveEndpointPort(l *adminapi.HostStatus) uint32 { return l.Address.GetSocketAddress().GetPortValue() } func retrieveEndpointStatus(l *adminapi.HostStatus) core.HealthStatus { return l.HealthStatus.GetEdsHealthStatus() } // Verify returns true if the passed host matches the filter fields func (e *EndpointFilter) Verify(host *adminapi.HostStatus, cluster string) bool { if e.Address == "" && e.Port == 0 && e.Cluster == "" && e.Status == "" { return true } if e.Address != "" && !strings.EqualFold(retrieveEndpointAddress(host), e.Address) { return false } if e.Port != 0 && retrieveEndpointPort(host) != e.Port { return false } if e.Cluster != "" && !strings.EqualFold(cluster, e.Cluster) { return false } status := retrieveEndpointStatus(host) if e.Status != "" && !strings.EqualFold(core.HealthStatus_name[int32(status)], e.Status) { return false } return true } // PrintEndpointsSummary prints just the endpoints config summary to the ConfigWriter stdout func (c *ConfigWriter) PrintEndpointsSummary(filter EndpointFilter) error { if c.clusters == nil { return fmt.Errorf("config writer has not been primed") } w := new(tabwriter.Writer).Init(c.Stdout, 0, 8, 5, ' ', 0) clusterEndpoint := make([]EndpointCluster, 0) for _, cluster := range c.clusters.ClusterStatuses { for _, host := range cluster.HostStatuses { if filter.Verify(host, cluster.Name) { addr := retrieveEndpointAddress(host) port := retrieveEndpointPort(host) status := retrieveEndpointStatus(host) clusterEndpoint = append(clusterEndpoint, EndpointCluster{addr, int(port), cluster.Name, status}) } } } clusterEndpoint = retrieveSortedEndpointClusterSlice(clusterEndpoint) fmt.Fprintln(w, "ENDPOINT\tSTATUS\tCLUSTER") for _, ce := range clusterEndpoint { endpoint := ce.address + ":" + strconv.Itoa(ce.port) fmt.Fprintf(w, "%v\t%v\t%v\n", endpoint, core.HealthStatus_name[int32(ce.status)], ce.cluster) } return w.Flush() } // PrintEndpoints prints the endpoints config to the ConfigWriter stdout func (c *ConfigWriter) PrintEndpoints(filter EndpointFilter) error { if c.clusters == nil { return fmt.Errorf("config writer has not been primed") } filteredClusters := protio.MessageSlice{} for _, cluster := range c.clusters.ClusterStatuses { for _, host := range cluster.HostStatuses { if filter.Verify(host, cluster.Name) { filteredClusters = append(filteredClusters, cluster) break } } } out, err := json.MarshalIndent(filteredClusters, "", " ") if err != nil { return err } fmt.Fprintln(c.Stdout, string(out)) return nil } func retrieveSortedEndpointClusterSlice(ec []EndpointCluster) []EndpointCluster { sort.Slice(ec, func(i, j int) bool { if ec[i].address == ec[j].address { if ec[i].port == ec[j].port { return ec[i].cluster < ec[j].cluster } return ec[i].port < ec[j].port } return ec[i].address < ec[j].address }) return ec }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "encoding/json" "fmt" "sort" "strings" "text/tabwriter" xdsapi "github.com/envoyproxy/go-control-plane/envoy/api/v2" protio "istio.io/istio/istioctl/pkg/util/proto" "istio.io/istio/pilot/pkg/model" ) // ClusterFilter is used to pass filter information into cluster based config writer print functions type ClusterFilter struct { FQDN model.Hostname Port int Subset string Direction model.TrafficDirection } // Verify returns true if the passed cluster matches the filter fields func (c *ClusterFilter) Verify(cluster *xdsapi.Cluster) bool { name := cluster.Name if c.FQDN == "" && c.Port == 0 && c.Subset == "" && c.Direction == "" { return true } if c.FQDN != "" && !strings.Contains(name, string(c.FQDN)) { return false } if c.Direction != "" && !strings.Contains(name, string(c.Direction)) { return false } if c.Subset != "" && !strings.Contains(name, c.Subset) { return false } if c.Port != 0 { p := fmt.Sprintf("|%v|", c.Port) if !strings.Contains(name, p) { return false } } return true } // PrintClusterSummary prints a summary of the relevant clusters in the config dump to the ConfigWriter stdout func (c *ConfigWriter) PrintClusterSummary(filter ClusterFilter) error { w, clusters, err := c.setupClusterConfigWriter() if err != nil { return err } fmt.Fprintln(w, "SERVICE FQDN\tPORT\tSUBSET\tDIRECTION\tTYPE") for _, cluster := range clusters { if filter.Verify(cluster) { if len(strings.Split(cluster.Name, "|")) > 3 { direction, subset, fqdn, port := model.ParseSubsetKey(cluster.Name) if subset == "" { subset = "-" } fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%s\n", fqdn, port, subset, direction, cluster.GetType()) } else { fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%s\n", cluster.Name, "-", "-", "-", cluster.GetType()) } } } return w.Flush() } // PrintClusterDump prints the relevant clusters in the config dump to the ConfigWriter stdout func (c *ConfigWriter) PrintClusterDump(filter ClusterFilter) error { _, clusters, err := c.setupClusterConfigWriter() if err != nil { return err } filteredClusters := protio.MessageSlice{} for _, cluster := range clusters { if filter.Verify(cluster) { filteredClusters = append(filteredClusters, cluster) } } out, err := json.MarshalIndent(filteredClusters, "", " ") if err != nil { return err } fmt.Fprintln(c.Stdout, string(out)) return nil } func (c *ConfigWriter) setupClusterConfigWriter() (*tabwriter.Writer, []*xdsapi.Cluster, error) { clusters, err := c.retrieveSortedClusterSlice() if err != nil { return nil, nil, err } w := new(tabwriter.Writer).Init(c.Stdout, 0, 8, 5, ' ', 0) return w, clusters, nil } func (c *ConfigWriter) retrieveSortedClusterSlice() ([]*xdsapi.Cluster, error) { if c.configDump == nil { return nil, fmt.Errorf("config writer has not been primed") } clusterDump, err := c.configDump.GetClusterConfigDump() if err != nil { return nil, err } clusters := make([]*xdsapi.Cluster, 0) for _, cluster := range clusterDump.DynamicActiveClusters { if cluster.Cluster != nil { clusters = append(clusters, cluster.Cluster) } } for _, cluster := range clusterDump.StaticClusters { if cluster.Cluster != nil { clusters = append(clusters, cluster.Cluster) } } if len(clusters) == 0 { return nil, fmt.Errorf("no clusters found") } sort.Slice(clusters, func(i, j int) bool { iDirection, iSubset, iName, iPort := safelyParseSubsetKey(clusters[i].Name) jDirection, jSubset, jName, jPort := safelyParseSubsetKey(clusters[j].Name) if iName == jName { if iSubset == jSubset { if iPort == jPort { return iDirection < jDirection } return iPort < jPort } return iSubset < jSubset } return iName < jName }) return clusters, nil } func safelyParseSubsetKey(key string) (model.TrafficDirection, string, model.Hostname, int) { if len(strings.Split(key, "|")) > 3 { return model.ParseSubsetKey(key) } name := model.Hostname(key) return model.TrafficDirection(""), "", name, 0 }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "encoding/json" "fmt" "io" "github.com/gogo/protobuf/jsonpb" "istio.io/istio/istioctl/pkg/util/configdump" ) // ConfigWriter is a writer for processing responses from the Envoy Admin config_dump endpoint type ConfigWriter struct { Stdout io.Writer configDump *configdump.Wrapper } // Prime loads the config dump into the writer ready for printing func (c *ConfigWriter) Prime(b []byte) error { cd := configdump.Wrapper{} // TODO(fisherxu): migrate this to jsonpb when issue fixed in golang // Issue to track -> https://github.com/golang/protobuf/issues/632 err := json.Unmarshal(b, &cd) if err != nil { return fmt.Errorf("error unmarshalling config dump response from Envoy: %v", err) } c.configDump = &cd return nil } // PrintBootstrapDump prints just the bootstrap config dump to the ConfigWriter stdout func (c *ConfigWriter) PrintBootstrapDump() error { if c.configDump == nil { return fmt.Errorf("config writer has not been primed") } bootstrapDump, err := c.configDump.GetBootstrapConfigDump() if err != nil { return err } jsonm := &jsonpb.Marshaler{Indent: " "} if err := jsonm.Marshal(c.Stdout, bootstrapDump); err != nil { return fmt.Errorf("unable to marshal bootstrap in Envoy config dump") } return nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "encoding/json" "fmt" "strings" "text/tabwriter" xdsapi "github.com/envoyproxy/go-control-plane/envoy/api/v2" protio "istio.io/istio/istioctl/pkg/util/proto" ) const ( // HTTPListener identifies a listener as being of HTTP type by the presence of an HTTP connection manager filter HTTPListener = "envoy.http_connection_manager" // TCPListener identifies a listener as being of TCP type by the presence of TCP proxy filter TCPListener = "envoy.tcp_proxy" ) // ListenerFilter is used to pass filter information into listener based config writer print functions type ListenerFilter struct { Address string Port uint32 Type string } // Verify returns true if the passed listener matches the filter fields func (l *ListenerFilter) Verify(listener *xdsapi.Listener) bool { if l.Address == "" && l.Port == 0 && l.Type == "" { return true } if l.Address != "" && !strings.EqualFold(retrieveListenerAddress(listener), l.Address) { return false } if l.Port != 0 && retrieveListenerPort(listener) != l.Port { return false } if l.Type != "" && !strings.EqualFold(retrieveListenerType(listener), l.Type) { return false } return true } func retrieveListenerType(l *xdsapi.Listener) string { for _, filterChain := range l.GetFilterChains() { for _, filter := range filterChain.GetFilters() { if filter.Name == HTTPListener { return "HTTP" } else if filter.Name == TCPListener { return "TCP" } } } return "UNKNOWN" } func retrieveListenerAddress(l *xdsapi.Listener) string { return l.Address.GetSocketAddress().Address } func retrieveListenerPort(l *xdsapi.Listener) uint32 { return l.Address.GetSocketAddress().GetPortValue() } // PrintListenerSummary prints a summary of the relevant listeners in the config dump to the ConfigWriter stdout func (c *ConfigWriter) PrintListenerSummary(filter ListenerFilter) error { w, listeners, err := c.setupListenerConfigWriter() if err != nil { return err } fmt.Fprintln(w, "ADDRESS\tPORT\tTYPE") for _, listener := range listeners { if filter.Verify(listener) { address := retrieveListenerAddress(listener) port := retrieveListenerPort(listener) listenerType := retrieveListenerType(listener) fmt.Fprintf(w, "%v\t%v\t%v\n", address, port, listenerType) } } return w.Flush() } // PrintListenerDump prints the relevant listeners in the config dump to the ConfigWriter stdout func (c *ConfigWriter) PrintListenerDump(filter ListenerFilter) error { _, listeners, err := c.setupListenerConfigWriter() if err != nil { return err } filteredListeners := protio.MessageSlice{} for _, listener := range listeners { if filter.Verify(listener) { filteredListeners = append(filteredListeners, listener) } } out, err := json.MarshalIndent(filteredListeners, "", " ") if err != nil { return err } fmt.Fprintln(c.Stdout, string(out)) return nil } func (c *ConfigWriter) setupListenerConfigWriter() (*tabwriter.Writer, []*xdsapi.Listener, error) { listeners, err := c.retrieveSortedListenerSlice() if err != nil { return nil, nil, err } w := new(tabwriter.Writer).Init(c.Stdout, 0, 8, 5, ' ', 0) return w, listeners, nil } func (c *ConfigWriter) retrieveSortedListenerSlice() ([]*xdsapi.Listener, error) { if c.configDump == nil { return nil, fmt.Errorf("config writer has not been primed") } listenerDump, err := c.configDump.GetListenerConfigDump() if err != nil { return nil, err } listeners := make([]*xdsapi.Listener, 0) for _, listener := range listenerDump.DynamicActiveListeners { if listener.Listener != nil { listeners = append(listeners, listener.Listener) } } for _, listener := range listenerDump.StaticListeners { if listener.Listener != nil { listeners = append(listeners, listener.Listener) } } if len(listeners) == 0 { return nil, fmt.Errorf("no listeners found") } return listeners, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package configdump import ( "encoding/json" "fmt" "sort" "strconv" "text/tabwriter" xdsapi "github.com/envoyproxy/go-control-plane/envoy/api/v2" protio "istio.io/istio/istioctl/pkg/util/proto" ) // RouteFilter is used to pass filter information into route based config writer print functions type RouteFilter struct { Name string } // Verify returns true if the passed route matches the filter fields func (r *RouteFilter) Verify(route *xdsapi.RouteConfiguration) bool { if r.Name != "" && r.Name != route.Name { return false } return true } // PrintRouteSummary prints a summary of the relevant routes in the config dump to the ConfigWriter stdout func (c *ConfigWriter) PrintRouteSummary(filter RouteFilter) error { w, routes, err := c.setupRouteConfigWriter() if err != nil { return err } fmt.Fprintln(c.Stdout, "NOTE: This output only contains routes loaded via RDS.") fmt.Fprintln(w, "NAME\tVIRTUAL HOSTS") for _, route := range routes { if filter.Verify(route) { fmt.Fprintf(w, "%v\t%v\n", route.Name, len(route.GetVirtualHosts())) } } return w.Flush() } // PrintRouteDump prints the relevant routes in the config dump to the ConfigWriter stdout func (c *ConfigWriter) PrintRouteDump(filter RouteFilter) error { _, routes, err := c.setupRouteConfigWriter() if err != nil { return err } filteredRoutes := protio.MessageSlice{} for _, route := range routes { if filter.Verify(route) { filteredRoutes = append(filteredRoutes, route) } } out, err := json.MarshalIndent(filteredRoutes, "", " ") if err != nil { return err } fmt.Fprintln(c.Stdout, string(out)) return nil } func (c *ConfigWriter) setupRouteConfigWriter() (*tabwriter.Writer, []*xdsapi.RouteConfiguration, error) { routes, err := c.retrieveSortedRouteSlice() if err != nil { return nil, nil, err } w := new(tabwriter.Writer).Init(c.Stdout, 0, 8, 5, ' ', 0) return w, routes, nil } func (c *ConfigWriter) retrieveSortedRouteSlice() ([]*xdsapi.RouteConfiguration, error) { if c.configDump == nil { return nil, fmt.Errorf("config writer has not been primed") } routeDump, err := c.configDump.GetRouteConfigDump() if err != nil { return nil, err } routes := make([]*xdsapi.RouteConfiguration, 0) for _, route := range routeDump.DynamicRouteConfigs { if route.RouteConfig != nil { routes = append(routes, route.RouteConfig) } } for _, route := range routeDump.StaticRouteConfigs { if route.RouteConfig != nil { routes = append(routes, route.RouteConfig) } } if len(routes) == 0 { return nil, fmt.Errorf("no routes found") } sort.Slice(routes, func(i, j int) bool { iName, err := strconv.Atoi(routes[i].Name) if err != nil { return false } jName, err := strconv.Atoi(routes[j].Name) if err != nil { return false } return iName < jName }) return routes, nil }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package pilot import ( "encoding/json" "fmt" "io" "sort" "text/tabwriter" v2 "istio.io/istio/pilot/pkg/proxy/envoy/v2" ) // TLSCheckWriter enables printing of tls-check using a single Pilot response type TLSCheckWriter struct { Writer io.Writer } func (t *TLSCheckWriter) setupTLSCheckPrint(authDebug []byte) (*tabwriter.Writer, []v2.AuthenticationDebug, error) { var dat []v2.AuthenticationDebug if err := json.Unmarshal(authDebug, &dat); err != nil { return nil, nil, err } if len(dat) < 1 { return nil, nil, fmt.Errorf("nothing to output") } sort.Slice(dat, func(i, j int) bool { if dat[i].Host == dat[j].Host { return dat[i].Port < dat[j].Port } return dat[i].Host < dat[j].Host }) w := new(tabwriter.Writer).Init(t.Writer, 0, 8, 5, ' ', 0) fmt.Fprintln(w, "HOST:PORT\tSTATUS\tSERVER\tCLIENT\tAUTHN POLICY\tDESTINATION RULE") return w, dat, nil } // PrintAll takes a Pilot authenticationz response and outputs them using a tabwriter func (t *TLSCheckWriter) PrintAll(authDebug []byte) error { w, fullAuth, err := t.setupTLSCheckPrint(authDebug) if err != nil { return err } for _, entry := range fullAuth { tlsCheckPrintln(w, entry) } return w.Flush() } // PrintSingle takes a Pilot authenticationz response and outputs them using a tabwriter filtering for a specific service func (t *TLSCheckWriter) PrintSingle(authDebug []byte, service string) error { w, fullAuth, err := t.setupTLSCheckPrint(authDebug) if err != nil { return err } for _, entry := range fullAuth { if entry.Host == service { tlsCheckPrintln(w, entry) break } } return w.Flush() } func tlsCheckPrintln(w io.Writer, entry v2.AuthenticationDebug) { if entry.Host == "" { return } host := fmt.Sprintf("%s:%d", entry.Host, entry.Port) fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", host, entry.TLSConflictStatus, entry.ServerProtocol, entry.ClientProtocol, entry.AuthenticationPolicyName, entry.DestinationRuleName) }
// Copyright 2018 Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package pilot import ( "encoding/json" "fmt" "io" "sort" "strings" "text/tabwriter" "time" v2 "istio.io/istio/pilot/pkg/proxy/envoy/v2" ) // StatusWriter enables printing of sync status using multiple []byte Pilot responses type StatusWriter struct { Writer io.Writer } type writerStatus struct { pilot string v2.SyncStatus } // PrintAll takes a slice of Pilot syncz responses and outputs them using a tabwriter func (s *StatusWriter) PrintAll(statuses map[string][]byte) error { w, fullStatus, err := s.setupStatusPrint(statuses) if err != nil { return err } for _, status := range fullStatus { if err := statusPrintln(w, status); err != nil { return err } } return w.Flush() } // PrintSingle takes a slice of Pilot syncz responses and outputs them using a tabwriter filtering for a specific pod func (s *StatusWriter) PrintSingle(statuses map[string][]byte, proxyName string) error { w, fullStatus, err := s.setupStatusPrint(statuses) if err != nil { return err } for _, status := range fullStatus { if strings.Contains(status.ProxyID, proxyName) { if err := statusPrintln(w, status); err != nil { return err } } } return w.Flush() } func (s *StatusWriter) setupStatusPrint(statuses map[string][]byte) (*tabwriter.Writer, []*writerStatus, error) { w := new(tabwriter.Writer).Init(s.Writer, 0, 8, 5, ' ', 0) fmt.Fprintln(w, "NAME\tCDS\tLDS\tEDS\tRDS\tPILOT\tVERSION") fullStatus := []*writerStatus{} for pilot, status := range statuses { ss := []*writerStatus{} err := json.Unmarshal(status, &ss) if err != nil { return nil, nil, err } for _, s := range ss { s.pilot = pilot } fullStatus = append(fullStatus, ss...) } sort.Slice(fullStatus, func(i, j int) bool { return fullStatus[i].ProxyID < fullStatus[j].ProxyID }) return w, fullStatus, nil } func statusPrintln(w io.Writer, status *writerStatus) error { clusterSynced := xdsStatus(status.ClusterSent, status.ClusterAcked) listenerSynced := xdsStatus(status.ListenerSent, status.ListenerAcked) routeSynced := xdsStatus(status.RouteSent, status.RouteAcked) endpointSynced := xdsStatus(status.EndpointSent, status.EndpointAcked) fmt.Fprintf(w, "%v\t%v\t%v\t%v (%v%%)\t%v\t%v\t%v\n", status.ProxyID, clusterSynced, listenerSynced, endpointSynced, status.EndpointPercent, routeSynced, status.pilot, status.ProxyVersion) return nil } func xdsStatus(sent, acked string) string { if sent == "" { return "NOT SENT" } if sent == acked { return "SYNCED" } timeSent, _ := parseTime(sent) timeAcked, _ := parseTime(acked) if timeAcked.Equal(time.Time{}) { return "STALE (Never Acknowledged)" } timeDiff := timeSent.Sub(timeAcked) return fmt.Sprintf("STALE (%v)", timeDiff.String()) } func parseTime(s string) (time.Time, error) { s = strings.Split(s, " m=+")[0] layout := "2006-01-02 15:04:05 +0000 MST" return time.Parse(layout, s) }