istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/cmd/mesh/install.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package mesh
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"sort"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/fatih/color"
    27  	"github.com/spf13/cobra"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  
    31  	"istio.io/api/operator/v1alpha1"
    32  	"istio.io/istio/istioctl/pkg/cli"
    33  	"istio.io/istio/istioctl/pkg/clioptions"
    34  	revtag "istio.io/istio/istioctl/pkg/tag"
    35  	"istio.io/istio/istioctl/pkg/util"
    36  	v1alpha12 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    37  	"istio.io/istio/operator/pkg/cache"
    38  	"istio.io/istio/operator/pkg/helmreconciler"
    39  	"istio.io/istio/operator/pkg/manifest"
    40  	"istio.io/istio/operator/pkg/name"
    41  	"istio.io/istio/operator/pkg/translate"
    42  	"istio.io/istio/operator/pkg/util/clog"
    43  	"istio.io/istio/operator/pkg/util/progress"
    44  	"istio.io/istio/operator/pkg/verifier"
    45  	pkgversion "istio.io/istio/operator/pkg/version"
    46  	operatorVer "istio.io/istio/operator/version"
    47  	"istio.io/istio/pkg/art"
    48  	"istio.io/istio/pkg/config/constants"
    49  	"istio.io/istio/pkg/config/labels"
    50  	"istio.io/istio/pkg/kube"
    51  )
    52  
    53  type InstallArgs struct {
    54  	// InFilenames is an array of paths to the input IstioOperator CR files.
    55  	InFilenames []string
    56  	// ReadinessTimeout is maximum time to wait for all Istio resources to be ready. wait must be true for this setting
    57  	// to take effect.
    58  	ReadinessTimeout time.Duration
    59  	// SkipConfirmation determines whether the user is prompted for confirmation.
    60  	// If set to true, the user is not prompted and a Yes response is assumed in all cases.
    61  	SkipConfirmation bool
    62  	// Force proceeds even if there are validation errors
    63  	Force bool
    64  	// Verify after installation
    65  	Verify bool
    66  	// Set is a string with element format "path=value" where path is an IstioOperator path and the value is a
    67  	// value to set the node at that path to.
    68  	Set []string
    69  	// ManifestsPath is a path to a ManifestsPath and profiles directory in the local filesystem with a release tgz.
    70  	ManifestsPath string
    71  	// Revision is the Istio control plane revision the command targets.
    72  	Revision string
    73  }
    74  
    75  func (a *InstallArgs) String() string {
    76  	var b strings.Builder
    77  	b.WriteString("InFilenames:      " + fmt.Sprint(a.InFilenames) + "\n")
    78  	b.WriteString("ReadinessTimeout: " + fmt.Sprint(a.ReadinessTimeout) + "\n")
    79  	b.WriteString("SkipConfirmation: " + fmt.Sprint(a.SkipConfirmation) + "\n")
    80  	b.WriteString("Force:            " + fmt.Sprint(a.Force) + "\n")
    81  	b.WriteString("Verify:           " + fmt.Sprint(a.Verify) + "\n")
    82  	b.WriteString("Set:              " + fmt.Sprint(a.Set) + "\n")
    83  	b.WriteString("ManifestsPath:    " + a.ManifestsPath + "\n")
    84  	b.WriteString("Revision:         " + a.Revision + "\n")
    85  	return b.String()
    86  }
    87  
    88  func addInstallFlags(cmd *cobra.Command, args *InstallArgs) {
    89  	cmd.PersistentFlags().StringSliceVarP(&args.InFilenames, "filename", "f", nil, filenameFlagHelpStr)
    90  	cmd.PersistentFlags().DurationVar(&args.ReadinessTimeout, "readiness-timeout", 300*time.Second,
    91  		"Maximum time to wait for Istio resources in each component to be ready.")
    92  	cmd.PersistentFlags().BoolVarP(&args.SkipConfirmation, "skip-confirmation", "y", false, skipConfirmationFlagHelpStr)
    93  	cmd.PersistentFlags().BoolVar(&args.Force, "force", false, ForceFlagHelpStr)
    94  	cmd.PersistentFlags().BoolVar(&args.Verify, "verify", false, VerifyCRInstallHelpStr)
    95  	cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
    96  	cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "charts", "", "", ChartsDeprecatedStr)
    97  	cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", ManifestsFlagHelpStr)
    98  	cmd.PersistentFlags().StringVarP(&args.Revision, "revision", "r", "", revisionFlagHelpStr)
    99  }
   100  
   101  // InstallCmdWithArgs generates an Istio install manifest and applies it to a cluster
   102  func InstallCmdWithArgs(ctx cli.Context, rootArgs *RootArgs, iArgs *InstallArgs) *cobra.Command {
   103  	ic := &cobra.Command{
   104  		Use:     "install",
   105  		Short:   "Applies an Istio manifest, installing or reconfiguring Istio on a cluster.",
   106  		Long:    "The install command generates an Istio install manifest and applies it to a cluster.",
   107  		Aliases: []string{"apply"},
   108  		// nolint: lll
   109  		Example: `  # Apply a default Istio installation
   110    istioctl install
   111  
   112    # Enable Tracing
   113    istioctl install --set meshConfig.enableTracing=true
   114  
   115    # Generate the demo profile and don't wait for confirmation
   116    istioctl install --set profile=demo --skip-confirmation
   117  
   118    # To override a setting that includes dots, escape them with a backslash (\).  Your shell may require enclosing quotes.
   119    istioctl install --set "values.sidecarInjectorWebhook.injectedAnnotations.container\.apparmor\.security\.beta\.kubernetes\.io/istio-proxy=runtime/default"
   120  `,
   121  		Args: cobra.ExactArgs(0),
   122  		PreRunE: func(cmd *cobra.Command, args []string) error {
   123  			if !labels.IsDNS1123Label(iArgs.Revision) && cmd.PersistentFlags().Changed("revision") {
   124  				return fmt.Errorf("invalid revision specified: %v", iArgs.Revision)
   125  			}
   126  			return nil
   127  		},
   128  		RunE: func(cmd *cobra.Command, args []string) error {
   129  			kubeClient, err := ctx.CLIClient()
   130  			if err != nil {
   131  				return err
   132  			}
   133  			l := clog.NewConsoleLogger(cmd.OutOrStdout(), cmd.ErrOrStderr(), installerScope)
   134  			p := NewPrinterForWriter(cmd.OutOrStderr())
   135  			p.Printf("%v\n", art.IstioColoredArt())
   136  			return Install(kubeClient, rootArgs, iArgs, cmd.OutOrStdout(), l, p)
   137  		},
   138  	}
   139  
   140  	addFlags(ic, rootArgs)
   141  	addInstallFlags(ic, iArgs)
   142  	return ic
   143  }
   144  
   145  // InstallCmd generates an Istio install manifest and applies it to a cluster
   146  func InstallCmd(ctx cli.Context) *cobra.Command {
   147  	return InstallCmdWithArgs(ctx, &RootArgs{}, &InstallArgs{})
   148  }
   149  
   150  func Install(kubeClient kube.CLIClient, rootArgs *RootArgs, iArgs *InstallArgs, stdOut io.Writer, l clog.Logger, p Printer,
   151  ) error {
   152  	kubeClient, client, err := KubernetesClients(kubeClient, l)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	tag, err := GetTagVersion(operatorVer.OperatorVersionString)
   158  	if err != nil {
   159  		return fmt.Errorf("fetch Istio version: %v", err)
   160  	}
   161  
   162  	// return warning if current date is near the EOL date
   163  	if operatorVer.IsEOL() {
   164  		warnMarker := color.New(color.FgYellow).Add(color.Italic).Sprint("WARNING:")
   165  		fmt.Printf("%s Istio %v may be out of support (EOL) already: see https://istio.io/latest/docs/releases/supported-releases/ for supported releases\n",
   166  			warnMarker, operatorVer.OperatorCodeBaseVersion)
   167  	}
   168  
   169  	setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath, iArgs.Revision)
   170  
   171  	_, iop, err := manifest.GenerateConfig(iArgs.InFilenames, setFlags, iArgs.Force, kubeClient, l)
   172  	if err != nil {
   173  		return fmt.Errorf("generate config: %v", err)
   174  	}
   175  
   176  	profile, ns, enabledComponents, err := getProfileNSAndEnabledComponents(iop)
   177  	if err != nil {
   178  		return fmt.Errorf("failed to get profile, namespace or enabled components: %v", err)
   179  	}
   180  
   181  	// Ignore the err because we don't want to show
   182  	// "no running Istio pods in istio-system" for the first time
   183  	_ = detectIstioVersionDiff(p, tag, ns, kubeClient, iop)
   184  	exists := revtag.PreviousInstallExists(context.Background(), kubeClient.Kube())
   185  	err = detectDefaultWebhookChange(p, kubeClient, iop, exists)
   186  	if err != nil {
   187  		return fmt.Errorf("failed to detect the default webhook change: %v", err)
   188  	}
   189  
   190  	// Warn users if they use `istioctl install` without any config args.
   191  	if !rootArgs.DryRun && !iArgs.SkipConfirmation {
   192  		prompt := fmt.Sprintf("This will install the Istio %s %q profile (with components: %s) into the cluster. Proceed? (y/N)",
   193  			tag, profile, humanReadableJoin(enabledComponents))
   194  		if !Confirm(prompt, stdOut) {
   195  			p.Println("Cancelled.")
   196  			os.Exit(1)
   197  		}
   198  	}
   199  
   200  	iop.Name = savedIOPName(iop)
   201  
   202  	// Detect whether previous installation exists prior to performing the installation.
   203  	if err := InstallManifests(iop, iArgs.Force, rootArgs.DryRun, kubeClient, client, iArgs.ReadinessTimeout, l); err != nil {
   204  		return fmt.Errorf("failed to install manifests: %v", err)
   205  	}
   206  	opts := &helmreconciler.ProcessDefaultWebhookOptions{
   207  		Namespace: ns,
   208  		DryRun:    rootArgs.DryRun,
   209  	}
   210  	if processed, err := helmreconciler.ProcessDefaultWebhook(kubeClient, iop, exists, opts); err != nil {
   211  		return fmt.Errorf("failed to process default webhook: %v", err)
   212  	} else if processed {
   213  		p.Println("Made this installation the default for cluster-wide operations.")
   214  	}
   215  
   216  	if iArgs.Verify {
   217  		if rootArgs.DryRun {
   218  			l.LogAndPrint("Control plane health check is not applicable in dry-run mode")
   219  			return nil
   220  		}
   221  		l.LogAndPrint("\n\nVerifying installation:")
   222  		installationVerifier, err := verifier.NewStatusVerifier(kubeClient, client, iop.Namespace, iArgs.ManifestsPath,
   223  			iArgs.InFilenames, clioptions.ControlPlaneOptions{Revision: iop.Spec.Revision},
   224  			verifier.WithLogger(l),
   225  			verifier.WithIOP(iop),
   226  		)
   227  		if err != nil {
   228  			return fmt.Errorf("failed to setup verifier: %v", err)
   229  		}
   230  		if err := installationVerifier.Verify(); err != nil {
   231  			return fmt.Errorf("verification failed with the following error: %v", err)
   232  		}
   233  	}
   234  
   235  	// Post-install message
   236  	if profile == "ambient" {
   237  		p.Println("The ambient profile has been installed successfully, enjoy Istio without sidecars!")
   238  	}
   239  	return nil
   240  }
   241  
   242  // InstallManifests generates manifests from the given istiooperator instance and applies them to the
   243  // cluster. See GenManifests for more description of the manifest generation process.
   244  //
   245  //	force   validation warnings are written to logger but command is not aborted
   246  //	DryRun  all operations are done but nothing is written
   247  //
   248  // Returns final IstioOperator after installation if successful.
   249  func InstallManifests(iop *v1alpha12.IstioOperator, force bool, dryRun bool, kubeClient kube.Client, client client.Client,
   250  	waitTimeout time.Duration, l clog.Logger,
   251  ) error {
   252  	// Needed in case we are running a test through this path that doesn't start a new process.
   253  	cache.FlushObjectCaches()
   254  	opts := &helmreconciler.Options{
   255  		DryRun: dryRun, Log: l, WaitTimeout: waitTimeout, ProgressLog: progress.NewLog(),
   256  		Force: force,
   257  	}
   258  	reconciler, err := helmreconciler.NewHelmReconciler(client, kubeClient, iop, opts)
   259  	if err != nil {
   260  		return err
   261  	}
   262  	status, err := reconciler.Reconcile()
   263  	if err != nil {
   264  		return fmt.Errorf("errors occurred during operation: %v", err)
   265  	}
   266  	if status.Status != v1alpha1.InstallStatus_HEALTHY {
   267  		return fmt.Errorf("errors occurred during operation")
   268  	}
   269  
   270  	// Previously we may install IOP file from the old version of istioctl. Now since we won't install IOP file
   271  	// anymore, and it didn't provide much value, we can delete it if it exists.
   272  	reconciler.DeleteIOPInClusterIfExists(iop)
   273  
   274  	opts.ProgressLog.SetState(progress.StateComplete)
   275  
   276  	return nil
   277  }
   278  
   279  func savedIOPName(iop *v1alpha12.IstioOperator) string {
   280  	ret := "installed-state"
   281  	if iop.Name != "" {
   282  		ret += "-" + iop.Name
   283  	}
   284  	if iop.Spec.Revision != "" {
   285  		ret += "-" + iop.Spec.Revision
   286  	}
   287  	return ret
   288  }
   289  
   290  // detectIstioVersionDiff will show warning if istioctl version and control plane version are different
   291  // nolint: interfacer
   292  func detectIstioVersionDiff(p Printer, tag string, ns string, kubeClient kube.CLIClient, iop *v1alpha12.IstioOperator) error {
   293  	warnMarker := color.New(color.FgYellow).Add(color.Italic).Sprint("WARNING:")
   294  	revision := iop.Spec.Revision
   295  	if revision == "" {
   296  		revision = util.DefaultRevisionName
   297  	}
   298  	icps, err := kubeClient.GetIstioVersions(context.TODO(), ns)
   299  	if err != nil {
   300  		return err
   301  	}
   302  	if len(*icps) != 0 {
   303  		var icpTags []string
   304  		var icpTag string
   305  		// create normalized tags for multiple control plane revisions
   306  		for _, icp := range *icps {
   307  			if icp.Revision != revision {
   308  				continue
   309  			}
   310  			tagVer, err := GetTagVersion(icp.Info.GitTag)
   311  			if err != nil {
   312  				return err
   313  			}
   314  			icpTags = append(icpTags, tagVer)
   315  		}
   316  		// sort different versions of control plane revisions
   317  		sort.Strings(icpTags)
   318  		// capture latest revision installed for comparison
   319  		for _, val := range icpTags {
   320  			if val != "" {
   321  				icpTag = val
   322  			}
   323  		}
   324  		// when the revision is passed
   325  		if icpTag != "" && tag != icpTag {
   326  			check := "         Before upgrading, you may wish to use 'istioctl x precheck' to check for upgrade warnings.\n"
   327  			revisionWarning := "         Running this command will overwrite it; use revisions to upgrade alongside the existing version.\n"
   328  			if revision != util.DefaultRevisionName {
   329  				revisionWarning = ""
   330  			}
   331  			if icpTag < tag {
   332  				p.Printf("%s Istio is being upgraded from %s to %s.\n"+revisionWarning+check,
   333  					warnMarker, icpTag, tag)
   334  			} else {
   335  				p.Printf("%s Istio is being downgraded from %s to %s.\n"+revisionWarning+check,
   336  					warnMarker, icpTag, tag)
   337  			}
   338  		}
   339  	}
   340  	return nil
   341  }
   342  
   343  // GetTagVersion returns istio tag version
   344  func GetTagVersion(tagInfo string) (string, error) {
   345  	if pkgversion.IsVersionString(tagInfo) {
   346  		tagInfo = pkgversion.TagToVersionStringGrace(tagInfo)
   347  	}
   348  	tag, err := pkgversion.NewVersionFromString(tagInfo)
   349  	if err != nil {
   350  		return "", err
   351  	}
   352  	return tag.String(), nil
   353  }
   354  
   355  // getProfileNSAndEnabledComponents get the profile and all the enabled components
   356  // from the given input files and --set flag overlays.
   357  func getProfileNSAndEnabledComponents(iop *v1alpha12.IstioOperator) (string, string, []string, error) {
   358  	var enabledComponents []string
   359  	if iop.Spec.Components != nil {
   360  		for _, c := range name.AllCoreComponentNames {
   361  			enabled, err := translate.IsComponentEnabledInSpec(c, iop.Spec)
   362  			if err != nil {
   363  				return "", "", nil, fmt.Errorf("failed to check if component: %s is enabled or not: %v", string(c), err)
   364  			}
   365  			if enabled {
   366  				enabledComponents = append(enabledComponents, name.UserFacingComponentName(c))
   367  			}
   368  		}
   369  		for _, c := range iop.Spec.Components.IngressGateways {
   370  			if c.Enabled.GetValue() {
   371  				enabledComponents = append(enabledComponents, name.UserFacingComponentName(name.IngressComponentName))
   372  				break
   373  			}
   374  		}
   375  		for _, c := range iop.Spec.Components.EgressGateways {
   376  			if c.Enabled.GetValue() {
   377  				enabledComponents = append(enabledComponents, name.UserFacingComponentName(name.EgressComponentName))
   378  				break
   379  			}
   380  		}
   381  	}
   382  
   383  	if configuredNamespace := v1alpha12.Namespace(iop.Spec); configuredNamespace != "" {
   384  		return iop.Spec.Profile, configuredNamespace, enabledComponents, nil
   385  	}
   386  	return iop.Spec.Profile, constants.IstioSystemNamespace, enabledComponents, nil
   387  }
   388  
   389  func humanReadableJoin(ss []string) string {
   390  	switch len(ss) {
   391  	case 0:
   392  		return ""
   393  	case 1:
   394  		return ss[0]
   395  	case 2:
   396  		return ss[0] + " and " + ss[1]
   397  	default:
   398  		return strings.Join(ss[:len(ss)-1], ", ") + ", and " + ss[len(ss)-1]
   399  	}
   400  }
   401  
   402  func detectDefaultWebhookChange(p Printer, client kube.CLIClient, iop *v1alpha12.IstioOperator, exists bool) error {
   403  	if !helmreconciler.DetectIfTagWebhookIsNeeded(iop, exists) {
   404  		return nil
   405  	}
   406  	mwhs, err := client.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), metav1.ListOptions{
   407  		LabelSelector: "app=sidecar-injector,istio.io/rev=default,istio.io/tag=default",
   408  	})
   409  	if err != nil {
   410  		return err
   411  	}
   412  	// If there is no default webhook but a revisioned default webhook exists,
   413  	// and we are installing a new IOP with default semantics, the default webhook shifts.
   414  	if exists && len(mwhs.Items) == 0 && iop.Spec.GetRevision() == "" {
   415  		p.Println("The default revision has been updated to point to this installation.")
   416  	}
   417  	return nil
   418  }