// 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)
}