blob: 166ff6a7fc7d5680c0d691dbd25c533d0e07b53d [file] [log] [blame]
// Copyright © 2021 NAME HERE <EMAIL ADDRESS>
//
// 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"
"errors"
"fmt"
"net"
"strconv"
"strings"
)
import (
adminapi "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
"github.com/fatih/color"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
authorizationapi "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
import (
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/clioptions"
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/install/k8sversion"
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/formatting"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis/analyzers/maturity"
"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"
kube3 "github.com/apache/dubbo-go-pixiu/pkg/config/legacy/source/kube"
"github.com/apache/dubbo-go-pixiu/pkg/config/resource"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/collections"
"github.com/apache/dubbo-go-pixiu/pkg/kube"
"github.com/apache/dubbo-go-pixiu/pkg/url"
"github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal"
)
func preCheck() *cobra.Command {
var opts clioptions.ControlPlaneOptions
var skipControlPlane bool
// cmd represents the upgradeCheck command
cmd := &cobra.Command{
Use: "precheck",
Short: "check whether Istio can safely be installed or upgrade",
Long: `precheck inspects a Kubernetes cluster for Istio install and upgrade requirements.`,
Example: ` # Verify that Istio can be installed or upgraded
istioctl x precheck
# Check only a single namespace
istioctl x precheck --namespace default`,
RunE: func(cmd *cobra.Command, args []string) (err error) {
cli, err := kube.NewExtendedClient(kube.BuildClientCmd(kubeconfig, configContext), revision)
if err != nil {
return err
}
msgs := diag.Messages{}
if !skipControlPlane {
msgs, err = checkControlPlane(cli)
if err != nil {
return err
}
}
nsmsgs, err := checkDataPlane(cli, namespace)
if err != nil {
return err
}
msgs.Add(nsmsgs...)
// Print all the messages to stdout in the specified format
msgs = msgs.SortedDedupedCopy()
output, err := formatting.Print(msgs, msgOutputFormat, colorize)
if err != nil {
return err
}
if len(msgs) == 0 {
fmt.Fprintf(cmd.ErrOrStderr(), color.New(color.FgGreen).Sprint("✔")+" No issues found when checking the cluster. Istio is safe to install or upgrade!\n"+
" To get started, check out https://istio.io/latest/docs/setup/getting-started/\n")
} else {
fmt.Fprintln(cmd.OutOrStdout(), output)
}
for _, m := range msgs {
if m.Type.Level().IsWorseThanOrEqualTo(diag.Warning) {
e := fmt.Sprintf(`Issues found when checking the cluster. Istio may not be safe to install or upgrade.
See %s for more information about causes and resolutions.`, url.ConfigAnalysis)
return errors.New(e)
}
}
return nil
},
}
cmd.PersistentFlags().BoolVar(&skipControlPlane, "skip-controlplane", false, "skip checking the control plane")
opts.AttachControlPlaneFlags(cmd)
return cmd
}
func checkControlPlane(cli kube.ExtendedClient) (diag.Messages, error) {
msgs := diag.Messages{}
m, err := checkServerVersion(cli)
if err != nil {
return nil, err
}
msgs = append(msgs, m...)
msgs = append(msgs, checkInstallPermissions(cli)...)
// TODO: add more checks
sa := local.NewSourceAnalyzer(analysis.Combine("upgrade precheck", &maturity.AlphaAnalyzer{}),
resource.Namespace(selectedNamespace), resource.Namespace(istioNamespace), nil, true, analysisTimeout)
// Set up the kube client
config := kube.BuildClientCmd(kubeconfig, configContext)
restConfig, err := config.ClientConfig()
if err != nil {
return nil, err
}
k, err := kube.NewClient(kube.NewClientConfigForRestConfig(restConfig))
if err != nil {
return nil, err
}
sa.AddRunningKubeSource(k)
cancel := make(chan struct{})
result, err := sa.Analyze(cancel)
if err != nil {
return nil, err
}
if result.Messages != nil {
msgs = append(msgs, result.Messages...)
}
return msgs, nil
}
func checkInstallPermissions(cli kube.ExtendedClient) diag.Messages {
Resources := []struct {
namespace string
group string
version string
name string
}{
{
version: "v1",
name: "Namespace",
},
{
namespace: istioNamespace,
group: "rbac.authorization.k8s.io",
version: "v1",
name: "ClusterRole",
},
{
namespace: istioNamespace,
group: "rbac.authorization.k8s.io",
version: "v1",
name: "ClusterRoleBinding",
},
{
namespace: istioNamespace,
group: "apiextensions.k8s.io",
version: "v1",
name: "CustomResourceDefinition",
},
{
namespace: istioNamespace,
group: "rbac.authorization.k8s.io",
version: "v1",
name: "Role",
},
{
namespace: istioNamespace,
version: "v1",
name: "ServiceAccount",
},
{
namespace: istioNamespace,
version: "v1",
name: "Service",
},
{
namespace: istioNamespace,
group: "apps",
version: "v1",
name: "Deployments",
},
{
namespace: istioNamespace,
version: "v1",
name: "ConfigMap",
},
{
group: "admissionregistration.k8s.io",
version: "v1",
name: "MutatingWebhookConfiguration",
},
{
group: "admissionregistration.k8s.io",
version: "v1",
name: "ValidatingWebhookConfiguration",
},
}
msgs := diag.Messages{}
for _, r := range Resources {
err := checkCanCreateResources(cli, r.namespace, r.group, r.version, r.name)
if err != nil {
msgs.Add(msg.NewInsufficientPermissions(&resource.Instance{Origin: clusterOrigin{}}, r.name, err.Error()))
}
}
return msgs
}
func checkCanCreateResources(c kube.ExtendedClient, namespace, group, version, name string) error {
s := &authorizationapi.SelfSubjectAccessReview{
Spec: authorizationapi.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
Namespace: namespace,
Verb: "create",
Group: group,
Version: version,
Resource: name,
},
},
}
response, err := c.AuthorizationV1().SelfSubjectAccessReviews().Create(context.Background(), s, metav1.CreateOptions{})
if err != nil {
return err
}
if !response.Status.Allowed {
if len(response.Status.Reason) > 0 {
return errors.New(response.Status.Reason)
}
return errors.New("permission denied")
}
return nil
}
func checkServerVersion(cli kube.ExtendedClient) (diag.Messages, error) {
v, err := cli.GetKubernetesVersion()
if err != nil {
return nil, fmt.Errorf("failed to get the Kubernetes version: %v", err)
}
compatible, err := k8sversion.CheckKubernetesVersion(v)
if err != nil {
return nil, err
}
if !compatible {
return []diag.Message{
msg.NewUnsupportedKubernetesVersion(&resource.Instance{Origin: clusterOrigin{}}, v.String(), fmt.Sprintf("1.%d", k8sversion.MinK8SVersion)),
}, nil
}
return nil, nil
}
func checkDataPlane(cli kube.ExtendedClient, namespace string) (diag.Messages, error) {
msgs := diag.Messages{}
m, err := checkListeners(cli, namespace)
if err != nil {
return nil, err
}
msgs = append(msgs, m...)
// TODO: add more checks
return msgs, nil
}
func checkListeners(cli kube.ExtendedClient, namespace string) (diag.Messages, error) {
pods, err := cli.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
// Find all running pods
FieldSelector: "status.phase=Running",
// Find all injected pods
LabelSelector: "security.istio.io/tlsMode=istio",
})
if err != nil {
return nil, err
}
var messages diag.Messages = make([]diag.Message, 0)
g := errgroup.Group{}
sem := semaphore.NewWeighted(25)
for _, pod := range pods.Items {
pod := pod
g.Go(func() error {
_ = sem.Acquire(context.Background(), 1)
defer sem.Release(1)
// Fetch list of all clusters to get which ports we care about
resp, err := cli.EnvoyDo(context.Background(), pod.Name, pod.Namespace, "GET", "config_dump?resource=dynamic_active_clusters&mask=cluster.name")
if err != nil {
fmt.Println("failed to get config dump: ", err)
return nil
}
ports, err := extractInboundPorts(resp)
if err != nil {
fmt.Println("failed to get ports: ", err)
return nil
}
// Next, look at what ports the pod is actually listening on
// This requires parsing the output from ss; the version we use doesn't support JSON
out, _, err := cli.PodExec(pod.Name, pod.Namespace, "istio-proxy", "ss -ltnH")
if err != nil {
if strings.Contains(err.Error(), "executable file not found") {
// Likely distroless or other custom build without ss. Nothing we can do here...
return nil
}
fmt.Println("failed to get listener state: ", err)
return nil
}
for _, ss := range strings.Split(out, "\n") {
if len(ss) == 0 {
continue
}
bind, port, err := net.SplitHostPort(getColumn(ss, 3))
if err != nil {
fmt.Println("failed to get parse state: ", err)
continue
}
ip := net.ParseIP(bind)
portn, _ := strconv.Atoi(port)
if _, f := ports[portn]; f {
c := ports[portn]
if bind == "" {
continue
} else if bind == "*" || ip.IsUnspecified() {
c.Wildcard = true
} else if ip.IsLoopback() {
c.Lo = true
} else {
c.Explicit = true
}
ports[portn] = c
}
}
origin := &kube3.Origin{
Collection: collections.K8SCoreV1Pods.Name(),
Kind: collections.K8SCoreV1Pods.Resource().Kind(),
FullName: resource.FullName{
Namespace: resource.Namespace(pod.Namespace),
Name: resource.LocalName(pod.Name),
},
Version: resource.Version(pod.ResourceVersion),
}
for port, status := range ports {
// Binding to localhost no longer works out of the box on Istio 1.10+, give them a warning.
if status.Lo {
messages.Add(msg.NewLocalhostListener(&resource.Instance{Origin: origin}, fmt.Sprint(port)))
}
}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return messages, nil
}
func getColumn(line string, col int) string {
res := []byte{}
prevSpace := false
for _, c := range line {
if col < 0 {
return string(res)
}
if c == ' ' {
if !prevSpace {
col--
}
prevSpace = true
continue
}
prevSpace = false
if col == 0 {
res = append(res, byte(c))
}
}
return string(res)
}
func extractInboundPorts(configdump []byte) (map[int]bindStatus, error) {
ports := map[int]bindStatus{}
cd := &adminapi.ConfigDump{}
if err := protomarshal.Unmarshal(configdump, cd); err != nil {
return nil, err
}
for _, cdump := range cd.Configs {
clw := &adminapi.ClustersConfigDump_DynamicCluster{}
if err := cdump.UnmarshalTo(clw); err != nil {
return nil, err
}
cl := &cluster.Cluster{}
if err := clw.Cluster.UnmarshalTo(cl); err != nil {
return nil, err
}
dir, _, _, port := model.ParseSubsetKey(cl.Name)
if dir == model.TrafficDirectionInbound {
ports[port] = bindStatus{}
}
}
return ports, nil
}
type bindStatus struct {
Lo bool
Wildcard bool
Explicit bool
}
func (b bindStatus) Any() bool {
return b.Lo || b.Wildcard || b.Explicit
}
func (b bindStatus) String() string {
res := []string{}
if b.Lo {
res = append(res, "Localhost")
}
if b.Wildcard {
res = append(res, "Wildcard")
}
if b.Explicit {
res = append(res, "Explicit")
}
if len(res) == 0 {
return "Unknown"
}
return strings.Join(res, ", ")
}
// clusterOrigin defines an Origin that refers to the cluster
type clusterOrigin struct{}
func (o clusterOrigin) String() string {
return ""
}
func (o clusterOrigin) FriendlyName() string {
return "Cluster"
}
func (o clusterOrigin) Comparator() string {
return o.FriendlyName()
}
func (o clusterOrigin) Namespace() resource.Namespace {
return ""
}
func (o clusterOrigin) Reference() resource.Reference {
return nil
}
func (o clusterOrigin) FieldMap() map[string]int {
return make(map[string]int)
}