blob: f3df01fe27abb456cf83ca721ed0673f453bf9bd [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 verifier
import (
"context"
"fmt"
"strings"
)
import (
"github.com/fatih/color"
"istio.io/api/label"
admit_v1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
v1batch "k8s.io/api/batch/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
apimachinery_schema "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/yaml"
)
import (
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/clioptions"
operator_istio "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio"
"github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1"
"github.com/apache/dubbo-go-pixiu/operator/pkg/controlplane"
"github.com/apache/dubbo-go-pixiu/operator/pkg/manifest"
"github.com/apache/dubbo-go-pixiu/operator/pkg/translate"
"github.com/apache/dubbo-go-pixiu/operator/pkg/util"
"github.com/apache/dubbo-go-pixiu/operator/pkg/util/clog"
"github.com/apache/dubbo-go-pixiu/pkg/kube"
)
var istioOperatorGVR = apimachinery_schema.GroupVersionResource{
Group: v1alpha1.SchemeGroupVersion.Group,
Version: v1alpha1.SchemeGroupVersion.Version,
Resource: "istiooperators",
}
// StatusVerifier checks status of certain resources like deployment,
// jobs and also verifies count of certain resource types.
type StatusVerifier struct {
istioNamespace string
manifestsPath string
filenames []string
controlPlaneOpts clioptions.ControlPlaneOptions
logger clog.Logger
iop *v1alpha1.IstioOperator
successMarker string
failureMarker string
client kube.ExtendedClient
}
type StatusVerifierOptions func(*StatusVerifier)
func WithLogger(l clog.Logger) StatusVerifierOptions {
return func(s *StatusVerifier) {
s.logger = l
}
}
func WithIOP(iop *v1alpha1.IstioOperator) StatusVerifierOptions {
return func(s *StatusVerifier) {
s.iop = iop
}
}
// NewStatusVerifier creates a new instance of post-install verifier
// which checks the status of various resources from the manifest.
func NewStatusVerifier(istioNamespace, manifestsPath, kubeconfig, context string,
filenames []string, controlPlaneOpts clioptions.ControlPlaneOptions,
options ...StatusVerifierOptions) (*StatusVerifier, error) {
client, err := kube.NewExtendedClient(kube.BuildClientCmd(kubeconfig, context), "")
if err != nil {
return nil, fmt.Errorf("failed to connect Kubernetes API server, error: %v", err)
}
verifier := StatusVerifier{
logger: clog.NewDefaultLogger(),
successMarker: "✔",
failureMarker: "✘",
istioNamespace: istioNamespace,
manifestsPath: manifestsPath,
filenames: filenames,
controlPlaneOpts: controlPlaneOpts,
client: client,
}
for _, opt := range options {
opt(&verifier)
}
return &verifier, nil
}
func (v *StatusVerifier) Colorize() {
v.successMarker = color.New(color.FgGreen).Sprint(v.successMarker)
v.failureMarker = color.New(color.FgRed).Sprint(v.failureMarker)
}
// Verify implements Verifier interface. Here we check status of deployment
// and jobs, count various resources for verification.
func (v *StatusVerifier) Verify() error {
if v.iop != nil {
return v.verifyFinalIOP()
}
if len(v.filenames) == 0 {
return v.verifyInstallIOPRevision()
}
return v.verifyInstall()
}
func (v *StatusVerifier) verifyInstallIOPRevision() error {
var err error
if v.controlPlaneOpts.Revision == "" {
v.controlPlaneOpts.Revision, err = v.getRevision()
if err != nil {
return err
}
}
iop, err := v.operatorFromCluster(v.controlPlaneOpts.Revision)
if err != nil {
// At this point we know there is no IstioOperator defining a control plane. This may
// be the case in a Istio cluster with external control plane.
v.logger.LogAndErrorf("error while fetching revision %s: %v", v.controlPlaneOpts.Revision, err.Error())
injector, err2 := v.injectorFromCluster(v.controlPlaneOpts.Revision)
if err2 == nil && injector != nil {
// The cluster *is* configured for Istio, but no IOP is present. This could mean
// - the user followed our remote control plane instructions
// - helm was used
// - user did `istioctl manifest generate | kubectl apply ...`
return fmt.Errorf("Istio present but verify-install needs an IstioOperator or manifest for comparison. Supply flag --filename <yaml>") // nolint: stylecheck
}
return fmt.Errorf("could not load IstioOperator from cluster: %v. Use --filename", err)
}
if v.manifestsPath != "" {
iop.Spec.InstallPackagePath = v.manifestsPath
}
profile := manifest.GetProfile(iop)
by, err := yaml.Marshal(iop)
if err != nil {
return err
}
mergedIOP, err := manifest.GetMergedIOP(string(by), profile, v.manifestsPath, v.controlPlaneOpts.Revision, v.client, v.logger)
if err != nil {
return err
}
crdCount, istioDeploymentCount, err := v.verifyPostInstallIstioOperator(
mergedIOP, fmt.Sprintf("in cluster operator %s", mergedIOP.GetName()))
return v.reportStatus(crdCount, istioDeploymentCount, err)
}
func (v *StatusVerifier) getRevision() (string, error) {
var revision string
var revs string
revCount := 0
pods, err := v.client.PodsForSelector(context.TODO(), v.istioNamespace, "app=istiod")
if err != nil {
return "", fmt.Errorf("failed to fetch istiod pod, error: %v", err)
}
for _, pod := range pods.Items {
rev := pod.ObjectMeta.GetLabels()[label.IoIstioRev.Name]
revCount++
if rev == "default" {
continue
}
revision = rev
}
if revision == "" {
revs = "default"
} else {
revs = revision
}
v.logger.LogAndPrintf("%d Istio control planes detected, checking --revision %q only", revCount, revs)
return revision, nil
}
func (v *StatusVerifier) verifyFinalIOP() error {
crdCount, istioDeploymentCount, err := v.verifyPostInstallIstioOperator(
v.iop, fmt.Sprintf("IOP:%s", v.iop.GetName()))
return v.reportStatus(crdCount, istioDeploymentCount, err)
}
func (v *StatusVerifier) verifyInstall() error {
// This is not a pre-check. Check that the supplied resources exist in the cluster
r := resource.NewBuilder(v.client.UtilFactory()).
Unstructured().
FilenameParam(false, &resource.FilenameOptions{Filenames: v.filenames}).
Flatten().
Do()
if r.Err() != nil {
return r.Err()
}
visitor := genericclioptions.ResourceFinderForResult(r).Do()
crdCount, istioDeploymentCount, err := v.verifyPostInstall(
visitor, strings.Join(v.filenames, ","))
return v.reportStatus(crdCount, istioDeploymentCount, err)
}
func (v *StatusVerifier) verifyPostInstallIstioOperator(iop *v1alpha1.IstioOperator, filename string) (int, int, error) {
t := translate.NewTranslator()
ver, err := v.client.GetKubernetesVersion()
if err != nil {
return 0, 0, err
}
cp, err := controlplane.NewIstioControlPlane(iop.Spec, t, nil, ver)
if err != nil {
return 0, 0, err
}
if err := cp.Run(); err != nil {
return 0, 0, err
}
manifests, errs := cp.RenderManifest()
if errs != nil && len(errs) > 0 {
return 0, 0, errs.ToError()
}
builder := resource.NewBuilder(v.client.UtilFactory()).ContinueOnError().Unstructured()
for cat, manifest := range manifests {
for i, manitem := range manifest {
reader := strings.NewReader(manitem)
pseudoFilename := fmt.Sprintf("%s:%d generated from %s", cat, i, filename)
builder = builder.Stream(reader, pseudoFilename)
}
}
r := builder.Flatten().Do()
if r.Err() != nil {
return 0, 0, r.Err()
}
visitor := genericclioptions.ResourceFinderForResult(r).Do()
// Indirectly RECURSE back into verifyPostInstall with the manifest we just generated
generatedCrds, generatedDeployments, err := v.verifyPostInstall(
visitor,
fmt.Sprintf("generated from %s", filename))
if err != nil {
return generatedCrds, generatedDeployments, err
}
return generatedCrds, generatedDeployments, nil
}
func (v *StatusVerifier) verifyPostInstall(visitor resource.Visitor, filename string) (int, int, error) {
crdCount := 0
istioDeploymentCount := 0
err := visitor.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
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(un.GetObjectKind().GroupVersionKind())
if kinds == "" {
kinds = strings.ToLower(kind) + "s"
}
if namespace == "" {
namespace = v.istioNamespace
}
switch kind {
case "Deployment":
deployment := &appsv1.Deployment{}
err = info.Client.
Get().
Resource(kinds).
Namespace(namespace).
Name(name).
VersionedParams(&meta_v1.GetOptions{}, scheme.ParameterCodec).
Do(context.TODO()).
Into(deployment)
if err != nil {
v.reportFailure(kind, name, namespace, err)
return err
}
if err = verifyDeploymentStatus(deployment); err != nil {
ivf := istioVerificationFailureError(filename, err)
v.reportFailure(kind, name, namespace, ivf)
return ivf
}
if namespace == v.istioNamespace && 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(context.TODO()).
Into(job)
if err != nil {
v.reportFailure(kind, name, namespace, err)
return err
}
if err := verifyJobPostInstall(job); err != nil {
ivf := istioVerificationFailureError(filename, err)
v.reportFailure(kind, name, namespace, ivf)
return ivf
}
case "IstioOperator":
// It is not a problem if the cluster does not include the IstioOperator
// we are checking. Instead, verify the cluster has the things the
// IstioOperator specifies it should have.
// IstioOperator isn't part of pkg/config/schema/collections,
// usual conversion not available. Convert unstructured to string
// and ask operator code to unmarshal.
fixTimestampRelatedUnmarshalIssues(un)
by := util.ToYAML(un)
unmergedIOP, err := operator_istio.UnmarshalIstioOperator(by, true)
if err != nil {
v.reportFailure(kind, name, namespace, err)
return err
}
profile := manifest.GetProfile(unmergedIOP)
iop, err := manifest.GetMergedIOP(by, profile, v.manifestsPath, v.controlPlaneOpts.Revision,
v.client, v.logger)
if err != nil {
v.reportFailure(kind, name, namespace, err)
return err
}
if v.manifestsPath != "" {
iop.Spec.InstallPackagePath = v.manifestsPath
}
if v1alpha1.Namespace(iop.Spec) == "" {
v1alpha1.SetNamespace(iop.Spec, v.istioNamespace)
}
generatedCrds, generatedDeployments, err := v.verifyPostInstallIstioOperator(iop, filename)
crdCount += generatedCrds
istioDeploymentCount += generatedDeployments
if err != nil {
return err
}
default:
result := info.Client.
Get().
Resource(kinds).
Name(name).
Do(context.TODO())
if result.Error() != nil {
result = info.Client.
Get().
Resource(kinds).
Namespace(namespace).
Name(name).
Do(context.TODO())
if result.Error() != nil {
v.reportFailure(kind, name, namespace, result.Error())
return istioVerificationFailureError(filename,
fmt.Errorf("the required %s:%s is not ready due to: %v",
kind, name, result.Error()))
}
}
if kind == "CustomResourceDefinition" {
crdCount++
}
}
v.logger.LogAndPrintf("%s %s: %s.%s checked successfully", v.successMarker, kind, name, namespace)
return nil
})
return crdCount, istioDeploymentCount, err
}
// Find Istio injector matching revision. ("" matches any revision.)
func (v *StatusVerifier) injectorFromCluster(revision string) (*admit_v1.MutatingWebhookConfiguration, error) {
hooks, err := v.client.AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), meta_v1.ListOptions{})
if err != nil {
return nil, err
}
revCount := 0
var hookmatch *admit_v1.MutatingWebhookConfiguration
for i, hook := range hooks.Items {
rev := hook.ObjectMeta.GetLabels()[label.IoIstioRev.Name]
if rev != "" {
revCount++
revision = rev
if revision == "" || revision == rev {
hookmatch = &hooks.Items[i]
}
}
}
v.logger.LogAndPrintf("%d Istio injectors detected", revCount)
if hookmatch != nil {
return hookmatch, nil
}
return nil, fmt.Errorf("Istio injector revision %q not found", revision) // nolint: stylecheck
}
// Find an IstioOperator matching revision in the cluster. The IstioOperators
// don't have a label for their revision, so we parse them and check .Spec.Revision
func (v *StatusVerifier) operatorFromCluster(revision string) (*v1alpha1.IstioOperator, error) {
iops, err := AllOperatorsInCluster(v.client.Dynamic())
if err != nil {
return nil, err
}
for _, iop := range iops {
if iop.Spec.Revision == revision ||
(iop.Spec.Revision == "default" && revision == "") ||
(iop.Spec.Revision == "" && revision == "default") {
return iop, nil
}
}
return nil, fmt.Errorf("control plane revision %q not found", revision)
}
func (v *StatusVerifier) reportStatus(crdCount, istioDeploymentCount int, err error) error {
v.logger.LogAndPrintf("Checked %v custom resource definitions", crdCount)
v.logger.LogAndPrintf("Checked %v Istio Deployments", istioDeploymentCount)
if istioDeploymentCount == 0 {
if err != nil {
v.logger.LogAndPrintf("! No Istio installation found: %v", err)
} else {
v.logger.LogAndPrintf("! No Istio installation found")
}
return fmt.Errorf("no Istio installation found")
}
if err != nil {
// Don't return full error; it is usually an unwieldy aggregate
return fmt.Errorf("Istio installation failed") // nolint
}
v.logger.LogAndPrintf("%s Istio is installed and verified successfully", v.successMarker)
return nil
}
func fixTimestampRelatedUnmarshalIssues(un *unstructured.Unstructured) {
un.SetCreationTimestamp(meta_v1.Time{}) // UnmarshalIstioOperator chokes on these
// UnmarshalIstioOperator fails because managedFields could contain time
// and gogo/protobuf/jsonpb(v1.3.1) tries to unmarshal it as struct (the type
// meta_v1.Time is really a struct) and fails.
un.SetManagedFields([]meta_v1.ManagedFieldsEntry{})
}
// Find all IstioOperator in the cluster.
func AllOperatorsInCluster(client dynamic.Interface) ([]*v1alpha1.IstioOperator, error) {
ul, err := client.
Resource(istioOperatorGVR).
List(context.TODO(), meta_v1.ListOptions{})
if err != nil {
return nil, err
}
retval := make([]*v1alpha1.IstioOperator, 0)
for _, un := range ul.Items {
fixTimestampRelatedUnmarshalIssues(&un)
by := util.ToYAML(un.Object)
iop, err := operator_istio.UnmarshalIstioOperator(by, true)
if err != nil {
return nil, err
}
retval = append(retval, iop)
}
return retval, nil
}
func istioVerificationFailureError(filename string, reason error) error {
return fmt.Errorf("Istio installation failed, incomplete or does not match \"%s\": %v", filename, reason) // nolint
}
func (v *StatusVerifier) reportFailure(kind, name, namespace string, err error) {
v.logger.LogAndPrintf("%s %s: %s.%s: %v", v.failureMarker, kind, name, namespace, err)
}