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 }