github.com/verrazzano/verrazzano@v1.7.0/tools/vz/cmd/install/install.go (about)

     1  // Copyright (c) 2022, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package install
     5  
     6  import (
     7  	"bufio"
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/spf13/cobra"
    14  	"github.com/verrazzano/verrazzano/pkg/kubectlutil"
    15  	"github.com/verrazzano/verrazzano/pkg/semver"
    16  	"github.com/verrazzano/verrazzano/platform-operator/apis/verrazzano/v1beta1"
    17  	"github.com/verrazzano/verrazzano/platform-operator/apis/verrazzano/validators"
    18  	"github.com/verrazzano/verrazzano/tools/vz/cmd/bugreport"
    19  	cmdhelpers "github.com/verrazzano/verrazzano/tools/vz/cmd/helpers"
    20  	"github.com/verrazzano/verrazzano/tools/vz/cmd/version"
    21  	"github.com/verrazzano/verrazzano/tools/vz/pkg/constants"
    22  	"github.com/verrazzano/verrazzano/tools/vz/pkg/helpers"
    23  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	"k8s.io/client-go/kubernetes"
    27  	"k8s.io/kube-openapi/pkg/util/proto/validation"
    28  	"k8s.io/kubectl/pkg/util/openapi"
    29  	clipkg "sigs.k8s.io/controller-runtime/pkg/client"
    30  )
    31  
    32  const (
    33  	CommandName = "install"
    34  	helpShort   = "Install Verrazzano"
    35  	helpLong    = `Install the Verrazzano Platform Operator and install the Verrazzano components specified by the Verrazzano CR provided on the command line`
    36  )
    37  
    38  var helpExample = fmt.Sprintf(`
    39  # Install the latest version of Verrazzano using the prod profile. Stream the logs to the console until the install completes.
    40  vz install
    41  
    42  # Install version %[1]s using a dev profile, timeout the command after 20 minutes.
    43  vz install --version v%[1]s --set profile=dev --timeout 20m
    44  
    45  # Install version %[1]s using a dev profile with kiali disabled and wait for the install to complete.
    46  vz install --version v%[1]s --set profile=dev --set components.kiali.enabled=false
    47  
    48  # Install the latest version of Verrazzano using CR overlays and explicit value sets.  Output the logs in json format.
    49  # The overlay files can be a comma-separated list or a series of -f options.  Both formats are shown.
    50  vz install -f base.yaml,custom.yaml --set profile=prod --log-format json
    51  vz install -f base.yaml -f custom.yaml --set profile=prod --log-format json
    52  # Install the latest version of Verrazzano with progress bar enabled.
    53  vz install --progress
    54  # Install the latest version of Verrazzano using a Verrazzano CR specified with stdin.
    55  vz install -f - <<EOF
    56  apiVersion: install.verrazzano.io/v1beta1
    57  kind: Verrazzano
    58  metadata:
    59    namespace: default
    60    name: example-verrazzano
    61  EOF`, version.GetCLIVersion())
    62  
    63  var logsEnum = cmdhelpers.LogFormatSimple
    64  
    65  // validateCR functions used for unit-tests
    66  type validateCRSig func(cmd *cobra.Command, obj *unstructured.Unstructured, vzHelper helpers.VZHelper) []error
    67  
    68  var ValidateCRFunc validateCRSig = ValidateCR
    69  
    70  func SetValidateCRFunc(f validateCRSig) {
    71  	ValidateCRFunc = f
    72  }
    73  
    74  func SetDefaultValidateCRFunc() {
    75  	ValidateCRFunc = ValidateCR
    76  }
    77  
    78  func FakeValidateCRFunc(cmd *cobra.Command, obj *unstructured.Unstructured, vzHelper helpers.VZHelper) []error {
    79  	return nil
    80  }
    81  
    82  func NewCmdInstall(vzHelper helpers.VZHelper) *cobra.Command {
    83  	cmd := cmdhelpers.NewCommand(vzHelper, CommandName, helpShort, helpLong)
    84  	cmd.RunE = func(cmd *cobra.Command, args []string) error {
    85  		return runCmdInstall(cmd, args, vzHelper)
    86  	}
    87  	cmd.Example = helpExample
    88  
    89  	cmd.PersistentFlags().Bool(constants.WaitFlag, constants.WaitFlagDefault, constants.WaitFlagHelp)
    90  	cmd.PersistentFlags().Duration(constants.TimeoutFlag, time.Minute*30, constants.TimeoutFlagHelp)
    91  	cmd.PersistentFlags().Duration(constants.VPOTimeoutFlag, time.Minute*5, constants.VPOTimeoutFlagHelp)
    92  	cmd.PersistentFlags().String(constants.VersionFlag, constants.VersionFlagDefault, constants.VersionFlagInstallHelp)
    93  	cmd.PersistentFlags().StringSliceP(constants.FilenameFlag, constants.FilenameFlagShorthand, []string{}, constants.FilenameFlagHelp)
    94  	cmd.PersistentFlags().Var(&logsEnum, constants.LogFormatFlag, constants.LogFormatHelp)
    95  	cmd.PersistentFlags().StringArrayP(constants.SetFlag, constants.SetFlagShorthand, []string{}, constants.SetFlagHelp)
    96  	cmd.PersistentFlags().Bool(constants.AutoBugReportFlag, constants.AutoBugReportFlagDefault, constants.AutoBugReportFlagHelp)
    97  	// Private registry support
    98  	cmd.PersistentFlags().String(constants.ImageRegistryFlag, constants.ImageRegistryFlagDefault, constants.ImageRegistryFlagHelp)
    99  	cmd.PersistentFlags().String(constants.ImagePrefixFlag, constants.ImagePrefixFlagDefault, constants.ImagePrefixFlagHelp)
   100  	cmd.PersistentFlags().BoolP(constants.ProgressFlag, constants.ProgressShorthand, constants.ProgressFlagDefault, constants.ProgressFlagHelp)
   101  	// Flag to skip any confirmation questions
   102  	cmd.PersistentFlags().BoolP(constants.SkipConfirmationFlag, constants.SkipConfirmationShort, false, constants.SkipConfirmationFlagHelp)
   103  	// Flag to skip reinstalling the Verrazzano Platform Operator
   104  	cmd.PersistentFlags().Bool(constants.SkipPlatformOperatorFlag, false, constants.SkipPlatformOperatorFlagHelp)
   105  	// Add flags related to specifying the platform operator manifests as a local file or a URL
   106  	cmdhelpers.AddManifestsFlags(cmd)
   107  
   108  	// Dry run flag is still being discussed - keep hidden for now
   109  	cmd.PersistentFlags().Bool(constants.DryRunFlag, false, "Simulate an install.")
   110  	cmd.PersistentFlags().MarkHidden(constants.DryRunFlag)
   111  
   112  	// Hide the flag for overriding the default wait timeout for the platform-operator
   113  	cmd.PersistentFlags().MarkHidden(constants.VPOTimeoutFlag)
   114  
   115  	// Verifies that the CLI args are not set at the creation of a command
   116  	vzHelper.VerifyCLIArgsNil(cmd)
   117  
   118  	return cmd
   119  }
   120  
   121  func runCmdInstall(cmd *cobra.Command, args []string, vzHelper helpers.VZHelper) error {
   122  	// Get the controller runtime client
   123  	client, err := vzHelper.GetClient(cmd)
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	// Validate the command options
   129  	err = validateCmd(cmd, client)
   130  	if err != nil {
   131  		return fmt.Errorf("Command validation failed: %s", err.Error())
   132  	}
   133  
   134  	// Get the timeout value for the install command
   135  	timeout, err := cmdhelpers.GetWaitTimeout(cmd, constants.TimeoutFlag)
   136  	if err != nil {
   137  		return err
   138  	}
   139  
   140  	// Get the log format value
   141  	logFormat, err := cmdhelpers.GetLogFormat(cmd)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	// Get the kubernetes clientset.  This will validate that the kubeconfig and context are valid.
   147  	kubeClient, err := vzHelper.GetKubeClient(cmd)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	// When manifests flag is not used, get the version from the command line
   153  	var version string
   154  	if !cmdhelpers.ManifestsFlagChanged(cmd) {
   155  		version, err = cmdhelpers.GetVersion(cmd, vzHelper)
   156  		if err != nil {
   157  			return err
   158  		}
   159  		fmt.Fprintf(vzHelper.GetOutputStream(), fmt.Sprintf("Installing Verrazzano version %s\n", version))
   160  	}
   161  
   162  	var vzNamespace string
   163  	var vzName string
   164  
   165  	// Get the VPO timeout
   166  	vpoTimeout, err := cmdhelpers.GetWaitTimeout(cmd, constants.VPOTimeoutFlag)
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	// Check to see if we have a vz resource already deployed
   172  	existingvz, _ := helpers.FindVerrazzanoResource(client)
   173  	if existingvz != nil {
   174  		// Allow install command to continue if an install is in progress and the same version is specified.
   175  		// For example, control-C was entered and the install command is run again.
   176  		// Note: "Installing" is a state that was used in pre 1.4.0 installs and replaced with "Reconciling".
   177  		if existingvz.Status.State != v1beta1.VzStateReconciling && existingvz.Status.State != "Installing" {
   178  			return fmt.Errorf("Only one install of Verrazzano is allowed")
   179  		}
   180  
   181  		if version != "" {
   182  			installVersion, err := semver.NewSemVersion(version)
   183  			if err != nil {
   184  				return fmt.Errorf("Failed creating semantic version from install version %s: %s", version, err.Error())
   185  			}
   186  			vzVersion, err := semver.NewSemVersion(existingvz.Status.Version)
   187  			if err != nil {
   188  				return fmt.Errorf("Failed creating semantic version from Verrazzano status version %s: %s", existingvz.Status.Version, err.Error())
   189  			}
   190  			if !installVersion.IsEqualTo(vzVersion) {
   191  				return fmt.Errorf("Unable to install version %s, install of version %s is in progress", version, existingvz.Status.Version)
   192  			}
   193  		}
   194  
   195  		if err := cmdhelpers.ValidatePrivateRegistry(cmd, client); err != nil {
   196  			skipConfirm, errConfirm := cmd.PersistentFlags().GetBool(constants.SkipConfirmationFlag)
   197  			if errConfirm != nil {
   198  				return errConfirm
   199  			}
   200  			proceed, err := cmdhelpers.ConfirmWithUser(vzHelper, fmt.Sprintf("%s\nYour new settings will be ignored. Continue?", err.Error()), skipConfirm)
   201  			if err != nil {
   202  				return err
   203  			}
   204  			if !proceed {
   205  				fmt.Fprintf(vzHelper.GetOutputStream(), "Operation canceled.")
   206  				return nil
   207  			}
   208  		}
   209  		fmt.Fprintf(vzHelper.GetOutputStream(), fmt.Sprintf("Install of Verrazzano version %s is already in progress\n", version))
   210  
   211  		vzNamespace = existingvz.Namespace
   212  		vzName = existingvz.Name
   213  	} else {
   214  		// Get the verrazzano install resource to be created
   215  		vz, obj, err := getVerrazzanoYAML(cmd, vzHelper, version)
   216  		if err != nil {
   217  			return err
   218  		}
   219  
   220  		// Determines whether to reapply the Verrazzano Platform Operator
   221  		continuePlatformOperatorReinstall, err := reapplyPlatformOperator(cmd, client)
   222  		if err != nil {
   223  			return err
   224  		}
   225  		if continuePlatformOperatorReinstall {
   226  			// Apply the Verrazzano operator.yaml.
   227  			err = cmdhelpers.ApplyPlatformOperatorYaml(cmd, client, vzHelper, version)
   228  			if err != nil {
   229  				return err
   230  			}
   231  		}
   232  		err = installVerrazzano(cmd, vzHelper, vz, client, version, vpoTimeout, obj, continuePlatformOperatorReinstall)
   233  		if err != nil {
   234  			return bugreport.AutoBugReport(cmd, vzHelper, err)
   235  		}
   236  		vzNamespace = vz.GetNamespace()
   237  		vzName = vz.GetName()
   238  	}
   239  	progressFlag, _ := cmd.PersistentFlags().GetBool(constants.ProgressFlag)
   240  	if progressFlag {
   241  		err = displayInstallationProgress(cmd, vzHelper, timeout)
   242  	} else {
   243  		// Wait for the Verrazzano install to complete and show the logs
   244  		err = waitForInstallToComplete(client, kubeClient, vzHelper, types.NamespacedName{Namespace: vzNamespace, Name: vzName}, timeout, vpoTimeout, logFormat)
   245  	}
   246  	if err != nil {
   247  		return bugreport.AutoBugReport(cmd, vzHelper, err)
   248  	}
   249  	return nil
   250  }
   251  
   252  func installVerrazzano(cmd *cobra.Command, vzHelper helpers.VZHelper, vz clipkg.Object, client clipkg.Client, version string, vpoTimeout time.Duration, obj *unstructured.Unstructured, continuePlatformOperatorReinstall bool) error {
   253  	// Determines whether to wait for Platform Operator
   254  	if continuePlatformOperatorReinstall {
   255  		// Wait for the platform operator to be ready before we create the Verrazzano resource.
   256  		_, err := cmdhelpers.WaitForPlatformOperator(client, vzHelper, v1beta1.CondInstallComplete, vpoTimeout)
   257  		if err != nil {
   258  			return err
   259  		}
   260  	}
   261  
   262  	// Validate Custom Resource if present
   263  	var errorArray = ValidateCRFunc(cmd, obj, vzHelper)
   264  	if len(errorArray) != 0 {
   265  		return fmt.Errorf("was unable to validate the given CR, the following error(s) occurred: \"%v\"", errorArray)
   266  	}
   267  
   268  	err := kubectlutil.SetLastAppliedConfigurationAnnotation(vz)
   269  	if err != nil {
   270  		return err
   271  	}
   272  
   273  	// Create the Verrazzano install resource, if need be.
   274  	// We will retry up to 5 times if there is an error.
   275  	// Sometimes we see intermittent webhook errors due to timeouts.
   276  	retry := 0
   277  	for {
   278  		err = client.Create(context.TODO(), vz)
   279  		if err != nil {
   280  			if retry == 5 {
   281  				return fmt.Errorf("Failed to create the verrazzano install resource: %s", err.Error())
   282  			}
   283  			time.Sleep(time.Second)
   284  			retry++
   285  			fmt.Fprintf(vzHelper.GetOutputStream(), fmt.Sprintf("Retrying after failing to create the verrazzano install resource: %s\n", err.Error()))
   286  			continue
   287  		}
   288  		break
   289  	}
   290  	return nil
   291  }
   292  
   293  // getVerrazzanoYAML returns the verrazzano install resource to be created
   294  func getVerrazzanoYAML(cmd *cobra.Command, vzHelper helpers.VZHelper, version string) (vz clipkg.Object, obj *unstructured.Unstructured, err error) {
   295  	// Get the list yaml filenames specified
   296  	filenames, err := cmd.PersistentFlags().GetStringSlice(constants.FilenameFlag)
   297  	if err != nil {
   298  		return nil, nil, err
   299  	}
   300  
   301  	// Get the set arguments - returning a list of properties and value
   302  	pvs, err := cmdhelpers.GetSetArguments(cmd, vzHelper)
   303  	if err != nil {
   304  		return nil, nil, err
   305  	}
   306  
   307  	// If no yamls files were passed on the command line then create a minimal verrazzano
   308  	// resource.  The minimal resource is used to create a resource called verrazzano
   309  	// in the default namespace using the prod profile.
   310  	var gv schema.GroupVersion
   311  	if len(filenames) == 0 {
   312  		gv, vz, err = helpers.NewVerrazzanoForVZVersion(version)
   313  		if err != nil {
   314  			return nil, nil, err
   315  		}
   316  	} else {
   317  		// Merge the yaml files passed on the command line
   318  		obj, err := cmdhelpers.MergeYAMLFiles(filenames, os.Stdin)
   319  		if err != nil {
   320  			return nil, nil, err
   321  		}
   322  		gv = obj.GroupVersionKind().GroupVersion()
   323  		vz = obj
   324  	}
   325  
   326  	// Generate yaml for the set flags passed on the command line
   327  	outYAML, err := cmdhelpers.GenerateYAMLForSetFlags(pvs)
   328  	if err != nil {
   329  		return nil, nil, err
   330  	}
   331  
   332  	// Merge the set flags passed on the command line. The set flags take precedence over
   333  	// the yaml files passed on the command line.
   334  	vz, unstructuredVZObj, err := cmdhelpers.MergeSetFlags(gv, vz, outYAML)
   335  	if err != nil {
   336  		return nil, nil, err
   337  	}
   338  
   339  	// Return the merged verrazzano install resource to be created
   340  	return vz, unstructuredVZObj, nil
   341  }
   342  
   343  // waitForInstallToComplete waits for the Verrazzano install to complete and shows the logs of
   344  // the ongoing Verrazzano install.
   345  func waitForInstallToComplete(client clipkg.Client, kubeClient kubernetes.Interface, vzHelper helpers.VZHelper, namespacedName types.NamespacedName, timeout time.Duration, vpoTimeout time.Duration, logFormat cmdhelpers.LogFormat) error {
   346  	return cmdhelpers.WaitForOperationToComplete(client, kubeClient, vzHelper, namespacedName, timeout, vpoTimeout, logFormat, v1beta1.CondInstallComplete)
   347  }
   348  
   349  // validateCmd - validate the command line options
   350  func validateCmd(cmd *cobra.Command, client clipkg.Client) error {
   351  	if cmd.PersistentFlags().Changed(constants.VersionFlag) && cmdhelpers.ManifestsFlagChanged(cmd) {
   352  		return fmt.Errorf("--%s and --%s cannot both be specified", constants.VersionFlag, constants.ManifestsFlag)
   353  	}
   354  	if cmd.PersistentFlags().Changed(constants.SkipPlatformOperatorFlag) && cmdhelpers.ManifestsFlagChanged(cmd) {
   355  		return fmt.Errorf("--%s and --%s cannot both be specified", constants.SkipPlatformOperatorFlag, constants.ManifestsFlag)
   356  	}
   357  	if cmd.PersistentFlags().Changed(constants.SkipPlatformOperatorFlag) {
   358  		// Get the list of platform operator pods currently running
   359  		vpoList, err := validators.GetPlatformOperatorPodList(client)
   360  		if err != nil {
   361  			return err
   362  		}
   363  		if len(vpoList.Items) == 0 {
   364  			return fmt.Errorf("--%s cannot be specified when there is no Verrazzano Platform Operator running", constants.SkipPlatformOperatorFlag)
   365  		}
   366  	}
   367  	prefix, err := cmd.PersistentFlags().GetString(constants.ImagePrefixFlag)
   368  	if err != nil {
   369  		return err
   370  	}
   371  	reg, err := cmd.PersistentFlags().GetString(constants.ImageRegistryFlag)
   372  	if err != nil {
   373  		return err
   374  	}
   375  	if prefix != constants.ImagePrefixFlagDefault && reg == constants.ImageRegistryFlagDefault {
   376  		return fmt.Errorf("%s cannot be specified without also specifying %s", constants.ImagePrefixFlag, constants.ImageRegistryFlag)
   377  	}
   378  	return nil
   379  }
   380  
   381  // validateCR - validates a Custom Resource before proceeding with an install
   382  func ValidateCR(cmd *cobra.Command, obj *unstructured.Unstructured, vzHelper helpers.VZHelper) []error {
   383  	discoveryClient, err := vzHelper.GetDiscoveryClient(cmd)
   384  	if err != nil {
   385  		return []error{err}
   386  	}
   387  	doc, err := discoveryClient.OpenAPISchema()
   388  	if err != nil {
   389  		return []error{err}
   390  	}
   391  	s, err := openapi.NewOpenAPIData(doc)
   392  	if err != nil {
   393  		return []error{err}
   394  	}
   395  
   396  	gvk := obj.GroupVersionKind()
   397  	schema := s.LookupResource(gvk)
   398  	if schema == nil {
   399  		return []error{fmt.Errorf("the schema for \"%v\" was not found", gvk.Kind)}
   400  	}
   401  
   402  	// ValidateModel validates a given schema
   403  	errorArray := validation.ValidateModel(obj.Object, schema, gvk.Kind)
   404  	if len(errorArray) != 0 {
   405  		return errorArray
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  // reapplyPlatformOperator - finds a running Verrazzano Platform Operator and determines whether to suppress the reinstall prompt
   412  func reapplyPlatformOperator(cmd *cobra.Command, client clipkg.Client) (bool, error) {
   413  	skipPlatformOperator, _ := cmd.Flags().GetBool(constants.SkipPlatformOperatorFlag)
   414  	skipConfirmation, _ := cmd.PersistentFlags().GetBool(constants.SkipConfirmationFlag)
   415  	vpoList, err := validators.GetPlatformOperatorPodList(client)
   416  	if err != nil {
   417  		return false, err
   418  	}
   419  	// Decide to suppress the reinstall prompt based on the skipPlatformOperatorFlag
   420  	// this is only valid if there is a Verrazzano Platform Operator already running,
   421  	// otherwise there would be no prompt to suppress
   422  	if len(vpoList.Items) > 0 {
   423  		continuePlatformOperatorReapply, err := continueReapply(skipPlatformOperator, skipConfirmation)
   424  		if err != nil {
   425  			return false, err
   426  		}
   427  		return continuePlatformOperatorReapply, nil
   428  	}
   429  	// DEFAULT scenario is reinstalling the platform operator
   430  	return true, nil
   431  }
   432  
   433  func continueReapply(skipPlatformOperator, skipConfirmation bool) (bool, error) {
   434  	if skipPlatformOperator {
   435  		return false, nil
   436  	}
   437  	if skipConfirmation {
   438  		return true, nil
   439  	}
   440  	var response string
   441  	scanner := bufio.NewScanner(os.Stdin)
   442  	fmt.Print("Do you want to reinstall the Verrazzano Platform Operator? [y/N]: ")
   443  	if scanner.Scan() {
   444  		response = scanner.Text()
   445  	}
   446  	if err := scanner.Err(); err != nil {
   447  		return false, err
   448  	}
   449  	if response == "y" || response == "Y" {
   450  		return true, nil
   451  	}
   452  	return false, nil
   453  }