blob: 01aba741b6b41d1ad7e4f1eef9d1ec46142063fe [file] [log] [blame]
// Copyright 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 cmd
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
)
import (
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
import (
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/formatting"
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/handlers"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis/analyzers"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis/diag"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis/local"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis/msg"
"github.com/apache/dubbo-go-pixiu/pkg/config/resource"
"github.com/apache/dubbo-go-pixiu/pkg/kube"
"github.com/apache/dubbo-go-pixiu/pkg/url"
)
// AnalyzerFoundIssuesError indicates that at least one analyzer found problems.
type AnalyzerFoundIssuesError struct{}
// FileParseError indicates a provided file was unable to be parsed.
type FileParseError struct{}
const FileParseString = "Some files couldn't be parsed."
func (f AnalyzerFoundIssuesError) Error() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Analyzers found issues when analyzing %s.\n", analyzeTargetAsString()))
sb.WriteString(fmt.Sprintf("See %s for more information about causes and resolutions.", url.ConfigAnalysis))
return sb.String()
}
func (f FileParseError) Error() string {
return FileParseString
}
var (
listAnalyzers bool
useKube bool
failureThreshold = formatting.MessageThreshold{Level: diag.Error} // messages at least this level will generate an error exit code
outputThreshold = formatting.MessageThreshold{Level: diag.Info} // messages at least this level will be included in the output
colorize bool
msgOutputFormat string
meshCfgFile string
selectedNamespace string
allNamespaces bool
suppress []string
analysisTimeout time.Duration
recursive bool
ignoreUnknown bool
fileExtensions = []string{".json", ".yaml", ".yml"}
)
// Analyze command
func Analyze() *cobra.Command {
analysisCmd := &cobra.Command{
Use: "analyze <file>...",
Short: "Analyze Istio configuration and print validation messages",
Example: ` # Analyze the current live cluster
istioctl analyze
# Analyze the current live cluster, simulating the effect of applying additional yaml files
istioctl analyze a.yaml b.yaml my-app-config/
# Analyze the current live cluster, simulating the effect of applying a directory of config recursively
istioctl analyze --recursive my-istio-config/
# Analyze yaml files without connecting to a live cluster
istioctl analyze --use-kube=false a.yaml b.yaml my-app-config/
# Analyze the current live cluster and suppress PodMissingProxy for pod mypod in namespace 'testing'.
istioctl analyze -S "IST0103=Pod mypod.testing"
# Analyze the current live cluster and suppress PodMissingProxy for all pods in namespace 'testing',
# and suppress MisplacedAnnotation on deployment foobar in namespace default.
istioctl analyze -S "IST0103=Pod *.testing" -S "IST0107=Deployment foobar.default"
# List available analyzers
istioctl analyze -L`,
RunE: func(cmd *cobra.Command, args []string) error {
msgOutputFormat = strings.ToLower(msgOutputFormat)
_, ok := formatting.MsgOutputFormats[msgOutputFormat]
if !ok {
return CommandParseError{
fmt.Errorf("%s not a valid option for format. See istioctl analyze --help", msgOutputFormat),
}
}
if listAnalyzers {
fmt.Print(AnalyzersAsString(analyzers.All()))
return nil
}
readers, err := gatherFiles(cmd, args)
if err != nil {
return err
}
cancel := make(chan struct{})
// We use the "namespace" arg that's provided as part of root istioctl as a flag for specifying what namespace to use
// for file resources that don't have one specified.
selectedNamespace = handlers.HandleNamespace(namespace, defaultNamespace)
// check whether selected namespace exists.
if namespace != "" && useKube {
client, err := kube.NewExtendedClient(kube.BuildClientCmd(kubeconfig, configContext), "")
if err != nil {
return err
}
_, err = client.CoreV1().Namespaces().Get(context.TODO(), namespace, v1.GetOptions{})
if errors.IsNotFound(err) {
fmt.Fprintf(cmd.ErrOrStderr(), "namespace %q not found\n", namespace)
return nil
}
}
// If we've explicitly asked for all namespaces, blank the selectedNamespace var out
if allNamespaces {
selectedNamespace = ""
}
sa := local.NewIstiodAnalyzer(analyzers.AllCombined(),
resource.Namespace(selectedNamespace),
resource.Namespace(istioNamespace), nil, true)
// Check for suppressions and add them to our SourceAnalyzer
suppressions := make([]local.AnalysisSuppression, 0, len(suppress))
for _, s := range suppress {
parts := strings.Split(s, "=")
if len(parts) != 2 {
return fmt.Errorf("%s is not a valid suppression value. See istioctl analyze --help", s)
}
// Check to see if the supplied code is valid. If not, emit a
// warning but continue.
codeIsValid := false
for _, at := range msg.All() {
if at.Code() == parts[0] {
codeIsValid = true
break
}
}
if !codeIsValid {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: Supplied message code '%s' is an unknown message code and will not have any effect.\n", parts[0])
}
suppressions = append(suppressions, local.AnalysisSuppression{
Code: parts[0],
ResourceName: parts[1],
})
}
sa.SetSuppressions(suppressions)
// If we're using kube, use that as a base source.
if useKube {
// Set up the kube client
restConfig, err := kube.DefaultRestConfig(kubeconfig, configContext)
if err != nil {
return err
}
k, err := kube.NewClient(kube.NewClientConfigForRestConfig(restConfig))
if err != nil {
return err
}
sa.AddRunningKubeSource(k)
}
// If we explicitly specify mesh config, use it.
// This takes precedence over default mesh config or mesh config from a running Kube instance.
if meshCfgFile != "" {
_ = sa.AddFileKubeMeshConfig(meshCfgFile)
}
// If we're not using kube (files only), add defaults for some resources we expect to be provided by Istio
if !useKube {
err := sa.AddDefaultResources()
if err != nil {
return err
}
}
// If files are provided, treat them (collectively) as a source.
parseErrors := 0
if len(readers) > 0 {
if err = sa.AddReaderKubeSource(readers); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Error(s) adding files: %v", err)
parseErrors++
}
}
// Do the analysis
result, err := sa.Analyze(cancel)
if err != nil {
return err
}
// Maybe output details about which analyzers ran
if verbose {
fmt.Fprintf(cmd.ErrOrStderr(), "Analyzed resources in %s\n", analyzeTargetAsString())
if len(result.SkippedAnalyzers) > 0 {
fmt.Fprintln(cmd.ErrOrStderr(), "Skipped analyzers:")
for _, a := range result.SkippedAnalyzers {
fmt.Fprintln(cmd.ErrOrStderr(), "\t", a)
}
}
if len(result.ExecutedAnalyzers) > 0 {
fmt.Fprintln(cmd.ErrOrStderr(), "Executed analyzers:")
for _, a := range result.ExecutedAnalyzers {
fmt.Fprintln(cmd.ErrOrStderr(), "\t", a)
}
}
fmt.Fprintln(cmd.ErrOrStderr())
}
// Get messages for output
outputMessages := result.Messages.SetDocRef("istioctl-analyze").FilterOutLowerThan(outputThreshold.Level)
// Print all the messages to stdout in the specified format
output, err := formatting.Print(outputMessages, msgOutputFormat, colorize)
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), output)
// An extra message on success
if len(outputMessages) == 0 {
if parseErrors == 0 {
if len(readers) > 0 {
var files []string
for _, r := range readers {
files = append(files, r.Name)
}
fmt.Fprintf(cmd.ErrOrStderr(), "\u2714 No validation issues found when analyzing %s.\n", strings.Join(files, "\n"))
} else {
fmt.Fprintf(cmd.ErrOrStderr(), "\u2714 No validation issues found when analyzing %s.\n", analyzeTargetAsString())
}
} else {
fileOrFiles := "files"
if parseErrors == 1 {
fileOrFiles = "file"
}
fmt.Fprintf(cmd.ErrOrStderr(),
"No validation issues found when analyzing %s (but %d %s could not be parsed).\n",
analyzeTargetAsString(),
parseErrors,
fileOrFiles,
)
}
}
// Return code is based on the unfiltered validation message list/parse errors
// We're intentionally keeping failure threshold and output threshold decoupled for now
var returnError error
if msgOutputFormat == formatting.LogFormat {
returnError = errorIfMessagesExceedThreshold(result.Messages)
if returnError == nil && parseErrors > 0 && !ignoreUnknown {
returnError = FileParseError{}
}
}
return returnError
},
}
analysisCmd.PersistentFlags().BoolVarP(&listAnalyzers, "list-analyzers", "L", false,
"List the analyzers available to run. Suppresses normal execution.")
analysisCmd.PersistentFlags().BoolVarP(&useKube, "use-kube", "k", true,
"Use live Kubernetes cluster for analysis. Set --use-kube=false to analyze files only.")
analysisCmd.PersistentFlags().BoolVar(&colorize, "color", formatting.IstioctlColorDefault(analysisCmd.OutOrStdout()),
"Default true. Disable with '=false' or set $TERM to dumb")
analysisCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
"Enable verbose output")
analysisCmd.PersistentFlags().Var(&failureThreshold, "failure-threshold",
fmt.Sprintf("The severity level of analysis at which to set a non-zero exit code. Valid values: %v", diag.GetAllLevelStrings()))
analysisCmd.PersistentFlags().Var(&outputThreshold, "output-threshold",
fmt.Sprintf("The severity level of analysis at which to display messages. Valid values: %v", diag.GetAllLevelStrings()))
analysisCmd.PersistentFlags().StringVarP(&msgOutputFormat, "output", "o", formatting.LogFormat,
fmt.Sprintf("Output format: one of %v", formatting.MsgOutputFormatKeys))
analysisCmd.PersistentFlags().StringVar(&meshCfgFile, "meshConfigFile", "",
"Overrides the mesh config values to use for analysis.")
analysisCmd.PersistentFlags().BoolVarP(&allNamespaces, "all-namespaces", "A", false,
"Analyze all namespaces")
analysisCmd.PersistentFlags().StringArrayVarP(&suppress, "suppress", "S", []string{},
"Suppress reporting a message code on a specific resource. Values are supplied in the form "+
`<code>=<resource> (e.g. '--suppress "IST0102=DestinationRule primary-dr.default"'). Can be repeated. `+
`You can include the wildcard character '*' to support a partial match (e.g. '--suppress "IST0102=DestinationRule *.default" ).`)
analysisCmd.PersistentFlags().DurationVar(&analysisTimeout, "timeout", 30*time.Second,
"The duration to wait before failing")
analysisCmd.PersistentFlags().BoolVarP(&recursive, "recursive", "R", false,
"Process directory arguments recursively. Useful when you want to analyze related manifests organized within the same directory.")
analysisCmd.PersistentFlags().BoolVar(&ignoreUnknown, "ignore-unknown", false,
"Don't complain about un-parseable input documents, for cases where analyze should run only on k8s compliant inputs.")
return analysisCmd
}
func gatherFiles(cmd *cobra.Command, args []string) ([]local.ReaderSource, error) {
var readers []local.ReaderSource
for _, f := range args {
var r *os.File
// Handle "-" as stdin as a special case.
if f == "-" {
if isatty.IsTerminal(os.Stdin.Fd()) && !isJSONorYAMLOutputFormat() {
fmt.Fprint(cmd.OutOrStdout(), "Reading from stdin:\n")
}
r = os.Stdin
readers = append(readers, local.ReaderSource{Name: f, Reader: r})
continue
}
fi, err := os.Stat(f)
if err != nil {
return nil, err
}
if fi.IsDir() {
dirReaders, err := gatherFilesInDirectory(cmd, f)
if err != nil {
return nil, err
}
readers = append(readers, dirReaders...)
} else {
if !isValidFile(f) {
fmt.Fprintf(cmd.ErrOrStderr(), "Skipping file %v, recognized file extensions are: %v\n", f, fileExtensions)
continue
}
rs, err := gatherFile(f)
if err != nil {
return nil, err
}
readers = append(readers, rs)
}
}
return readers, nil
}
func gatherFile(f string) (local.ReaderSource, error) {
r, err := os.Open(f)
if err != nil {
return local.ReaderSource{}, err
}
runtime.SetFinalizer(r, func(x *os.File) { x.Close() })
return local.ReaderSource{Name: f, Reader: r}, nil
}
func gatherFilesInDirectory(cmd *cobra.Command, dir string) ([]local.ReaderSource, error) {
var readers []local.ReaderSource
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// If we encounter a directory, recurse only if the --recursve option
// was provided and the directory is not the same as dir.
if info.IsDir() {
if !recursive && dir != path {
return filepath.SkipDir
}
return nil
}
if !isValidFile(path) {
fmt.Fprintf(cmd.ErrOrStderr(), "Skipping file %v, recognized file extensions are: %v\n", path, fileExtensions)
return nil
}
r, err := os.Open(path)
if err != nil {
return err
}
runtime.SetFinalizer(r, func(x *os.File) { x.Close() })
readers = append(readers, local.ReaderSource{Name: path, Reader: r})
return nil
})
return readers, err
}
func errorIfMessagesExceedThreshold(messages []diag.Message) error {
foundIssues := false
for _, m := range messages {
if m.Type.Level().IsWorseThanOrEqualTo(failureThreshold.Level) {
foundIssues = true
}
}
if foundIssues {
return AnalyzerFoundIssuesError{}
}
return nil
}
func isValidFile(f string) bool {
ext := filepath.Ext(f)
for _, e := range fileExtensions {
if e == ext {
return true
}
}
return false
}
func AnalyzersAsString(analyzers []analysis.Analyzer) string {
nameToAnalyzer := make(map[string]analysis.Analyzer)
analyzerNames := make([]string, len(analyzers))
for i, a := range analyzers {
analyzerNames[i] = a.Metadata().Name
nameToAnalyzer[a.Metadata().Name] = a
}
sort.Strings(analyzerNames)
var b strings.Builder
for _, aName := range analyzerNames {
b.WriteString(fmt.Sprintf("* %s:\n", aName))
a := nameToAnalyzer[aName]
if a.Metadata().Description != "" {
b.WriteString(fmt.Sprintf(" %s\n", a.Metadata().Description))
}
}
return b.String()
}
func analyzeTargetAsString() string {
if allNamespaces {
return "all namespaces"
}
return fmt.Sprintf("namespace: %s", selectedNamespace)
}
// TODO: Refactor output writer so that it is smart enough to know when to output what.
func isJSONorYAMLOutputFormat() bool {
return msgOutputFormat == formatting.JSONFormat || msgOutputFormat == formatting.YAMLFormat
}