github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/sync/create.go (about)

     1  package sync
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"strings"
     9  
    10  	"github.com/spf13/cobra"
    11  	"github.com/spf13/pflag"
    12  
    13  	"google.golang.org/grpc"
    14  
    15  	"github.com/dustin/go-humanize"
    16  
    17  	"github.com/mutagen-io/mutagen/cmd"
    18  	"github.com/mutagen-io/mutagen/cmd/mutagen/daemon"
    19  
    20  	"github.com/mutagen-io/mutagen/pkg/configuration/global"
    21  	"github.com/mutagen-io/mutagen/pkg/filesystem"
    22  	"github.com/mutagen-io/mutagen/pkg/filesystem/behavior"
    23  	"github.com/mutagen-io/mutagen/pkg/grpcutil"
    24  	"github.com/mutagen-io/mutagen/pkg/selection"
    25  	promptingsvc "github.com/mutagen-io/mutagen/pkg/service/prompting"
    26  	synchronizationsvc "github.com/mutagen-io/mutagen/pkg/service/synchronization"
    27  	"github.com/mutagen-io/mutagen/pkg/synchronization"
    28  	"github.com/mutagen-io/mutagen/pkg/synchronization/compression"
    29  	"github.com/mutagen-io/mutagen/pkg/synchronization/core"
    30  	"github.com/mutagen-io/mutagen/pkg/synchronization/core/ignore"
    31  	"github.com/mutagen-io/mutagen/pkg/synchronization/hashing"
    32  	"github.com/mutagen-io/mutagen/pkg/url"
    33  )
    34  
    35  // loadAndValidateGlobalSynchronizationConfiguration loads a YAML-based global
    36  // configuration, extracts the synchronization component, converts it to a
    37  // Protocol Buffers session configuration, and validates it.
    38  func loadAndValidateGlobalSynchronizationConfiguration(path string) (*synchronization.Configuration, error) {
    39  	// Load the YAML configuration.
    40  	yamlConfiguration, err := global.LoadConfiguration(path)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  
    45  	// Convert the YAML configuration to a Protocol Buffers representation and
    46  	// validate it.
    47  	configuration := yamlConfiguration.Synchronization.Defaults.ToInternal()
    48  	if err := configuration.EnsureValid(false); err != nil {
    49  		return nil, fmt.Errorf("invalid configuration: %w", err)
    50  	}
    51  
    52  	// Success.
    53  	return configuration, nil
    54  }
    55  
    56  // CreateWithSpecification is an orchestration convenience method that performs
    57  // a create operation using the provided daemon connection and session
    58  // specification.
    59  func CreateWithSpecification(
    60  	daemonConnection *grpc.ClientConn,
    61  	specification *synchronizationsvc.CreationSpecification,
    62  ) (string, error) {
    63  	// Initiate command line prompting.
    64  	statusLinePrinter := &cmd.StatusLinePrinter{}
    65  	promptingCtx, promptingCancel := context.WithCancel(context.Background())
    66  	prompter, promptingErrors, err := promptingsvc.Host(
    67  		promptingCtx, promptingsvc.NewPromptingClient(daemonConnection),
    68  		&cmd.StatusLinePrompter{Printer: statusLinePrinter}, true,
    69  	)
    70  	if err != nil {
    71  		promptingCancel()
    72  		return "", fmt.Errorf("unable to initiate prompting: %w", err)
    73  	}
    74  
    75  	// Perform the create operation, cancel prompting, and handle errors.
    76  	synchronizationService := synchronizationsvc.NewSynchronizationClient(daemonConnection)
    77  	request := &synchronizationsvc.CreateRequest{
    78  		Prompter:      prompter,
    79  		Specification: specification,
    80  	}
    81  	response, err := synchronizationService.Create(context.Background(), request)
    82  	promptingCancel()
    83  	<-promptingErrors
    84  	if err != nil {
    85  		statusLinePrinter.BreakIfPopulated()
    86  		return "", grpcutil.PeelAwayRPCErrorLayer(err)
    87  	} else if err = response.EnsureValid(); err != nil {
    88  		statusLinePrinter.BreakIfPopulated()
    89  		return "", fmt.Errorf("invalid create response received: %w", err)
    90  	}
    91  
    92  	// Success.
    93  	statusLinePrinter.Clear()
    94  	return response.Session, nil
    95  }
    96  
    97  // createMain is the entry point for the create command.
    98  func createMain(_ *cobra.Command, arguments []string) error {
    99  	// Validate, extract, and parse URLs.
   100  	if len(arguments) != 2 {
   101  		return errors.New("invalid number of endpoint URLs provided")
   102  	}
   103  	alpha, err := url.Parse(arguments[0], url.Kind_Synchronization, true)
   104  	if err != nil {
   105  		return fmt.Errorf("unable to parse alpha URL: %w", err)
   106  	}
   107  	beta, err := url.Parse(arguments[1], url.Kind_Synchronization, false)
   108  	if err != nil {
   109  		return fmt.Errorf("unable to parse beta URL: %w", err)
   110  	}
   111  
   112  	// Validate the name.
   113  	if err := selection.EnsureNameValid(createConfiguration.name); err != nil {
   114  		return fmt.Errorf("invalid session name: %w", err)
   115  	}
   116  
   117  	// Parse, validate, and record labels.
   118  	var labels map[string]string
   119  	if len(createConfiguration.labels) > 0 {
   120  		labels = make(map[string]string, len(createConfiguration.labels))
   121  	}
   122  	for _, label := range createConfiguration.labels {
   123  		components := strings.SplitN(label, "=", 2)
   124  		var key, value string
   125  		key = components[0]
   126  		if len(components) == 2 {
   127  			value = components[1]
   128  		}
   129  		if err := selection.EnsureLabelKeyValid(key); err != nil {
   130  			return fmt.Errorf("invalid label key: %w", err)
   131  		} else if err := selection.EnsureLabelValueValid(value); err != nil {
   132  			return fmt.Errorf("invalid label value: %w", err)
   133  		}
   134  		labels[key] = value
   135  	}
   136  
   137  	// Create a default session configuration that will form the basis of our
   138  	// cumulative configuration.
   139  	configuration := &synchronization.Configuration{}
   140  
   141  	// Unless disabled, attempt to load configuration from the global
   142  	// configuration file and merge it into our cumulative configuration.
   143  	if !createConfiguration.noGlobalConfiguration {
   144  		// Compute the path to the global configuration file.
   145  		globalConfigurationPath, err := global.ConfigurationPath()
   146  		if err != nil {
   147  			return fmt.Errorf("unable to compute path to global configuration file: %w", err)
   148  		}
   149  
   150  		// Attempt to load the file. We allow it to not exist.
   151  		globalConfiguration, err := loadAndValidateGlobalSynchronizationConfiguration(globalConfigurationPath)
   152  		if err != nil {
   153  			if !os.IsNotExist(err) {
   154  				return fmt.Errorf("unable to load global configuration: %w", err)
   155  			}
   156  		} else {
   157  			configuration = synchronization.MergeConfigurations(configuration, globalConfiguration)
   158  		}
   159  	}
   160  
   161  	// If additional default configuration files have been specified, then load
   162  	// them and merge them into the cumulative configuration.
   163  	for _, configurationFile := range createConfiguration.configurationFiles {
   164  		if c, err := loadAndValidateGlobalSynchronizationConfiguration(configurationFile); err != nil {
   165  			return fmt.Errorf("unable to load configuration file (%s): %w", configurationFile, err)
   166  		} else {
   167  			configuration = synchronization.MergeConfigurations(configuration, c)
   168  		}
   169  	}
   170  
   171  	// Validate and convert the synchronization mode specification.
   172  	var synchronizationMode core.SynchronizationMode
   173  	if createConfiguration.synchronizationMode != "" {
   174  		if err := synchronizationMode.UnmarshalText([]byte(createConfiguration.synchronizationMode)); err != nil {
   175  			return fmt.Errorf("unable to parse synchronization mode: %w", err)
   176  		}
   177  	}
   178  
   179  	// Validate and convert the hashing algorithm specification.
   180  	var hashingAlgorithm hashing.Algorithm
   181  	if createConfiguration.hash != "" {
   182  		if err := hashingAlgorithm.UnmarshalText([]byte(createConfiguration.hash)); err != nil {
   183  			return fmt.Errorf("unable to parse hashing algorithm: %w", err)
   184  		}
   185  	}
   186  
   187  	// There's no need to validate the maximum entry count - any uint64 value is
   188  	// valid.
   189  
   190  	// Validate and convert the maximum staging file size.
   191  	var maximumStagingFileSize uint64
   192  	if createConfiguration.maximumStagingFileSize != "" {
   193  		if s, err := humanize.ParseBytes(createConfiguration.maximumStagingFileSize); err != nil {
   194  			return fmt.Errorf("unable to parse maximum staging file size: %w", err)
   195  		} else {
   196  			maximumStagingFileSize = s
   197  		}
   198  	}
   199  
   200  	// Validate and convert probe mode specifications.
   201  	var probeMode, probeModeAlpha, probeModeBeta behavior.ProbeMode
   202  	if createConfiguration.probeMode != "" {
   203  		if err := probeMode.UnmarshalText([]byte(createConfiguration.probeMode)); err != nil {
   204  			return fmt.Errorf("unable to parse probe mode: %w", err)
   205  		}
   206  	}
   207  	if createConfiguration.probeModeAlpha != "" {
   208  		if err := probeModeAlpha.UnmarshalText([]byte(createConfiguration.probeModeAlpha)); err != nil {
   209  			return fmt.Errorf("unable to parse probe mode for alpha: %w", err)
   210  		}
   211  	}
   212  	if createConfiguration.probeModeBeta != "" {
   213  		if err := probeModeBeta.UnmarshalText([]byte(createConfiguration.probeModeBeta)); err != nil {
   214  			return fmt.Errorf("unable to parse probe mode for beta: %w", err)
   215  		}
   216  	}
   217  
   218  	// Validate and convert scan mode specifications.
   219  	var scanMode, scanModeAlpha, scanModeBeta synchronization.ScanMode
   220  	if createConfiguration.scanMode != "" {
   221  		if err := scanMode.UnmarshalText([]byte(createConfiguration.scanMode)); err != nil {
   222  			return fmt.Errorf("unable to parse scan mode: %w", err)
   223  		}
   224  	}
   225  	if createConfiguration.scanModeAlpha != "" {
   226  		if err := scanModeAlpha.UnmarshalText([]byte(createConfiguration.scanModeAlpha)); err != nil {
   227  			return fmt.Errorf("unable to parse scan mode for alpha: %w", err)
   228  		}
   229  	}
   230  	if createConfiguration.scanModeBeta != "" {
   231  		if err := scanModeBeta.UnmarshalText([]byte(createConfiguration.scanModeBeta)); err != nil {
   232  			return fmt.Errorf("unable to parse scan mode for beta: %w", err)
   233  		}
   234  	}
   235  
   236  	// Validate and convert staging mode specifications.
   237  	var stageMode, stageModeAlpha, stageModeBeta synchronization.StageMode
   238  	if createConfiguration.stageMode != "" {
   239  		if err := stageMode.UnmarshalText([]byte(createConfiguration.stageMode)); err != nil {
   240  			return fmt.Errorf("unable to parse staging mode: %w", err)
   241  		}
   242  	}
   243  	if createConfiguration.stageModeAlpha != "" {
   244  		if err := stageModeAlpha.UnmarshalText([]byte(createConfiguration.stageModeAlpha)); err != nil {
   245  			return fmt.Errorf("unable to parse staging mode for alpha: %w", err)
   246  		}
   247  	}
   248  	if createConfiguration.stageModeBeta != "" {
   249  		if err := stageModeBeta.UnmarshalText([]byte(createConfiguration.stageModeBeta)); err != nil {
   250  			return fmt.Errorf("unable to parse staging mode for beta: %w", err)
   251  		}
   252  	}
   253  
   254  	// Validate and convert the symbolic link mode specification.
   255  	var symbolicLinkMode core.SymbolicLinkMode
   256  	if createConfiguration.symbolicLinkMode != "" {
   257  		if err := symbolicLinkMode.UnmarshalText([]byte(createConfiguration.symbolicLinkMode)); err != nil {
   258  			return fmt.Errorf("unable to parse symbolic link mode: %w", err)
   259  		}
   260  	}
   261  
   262  	// Validate and convert watch mode specifications.
   263  	var watchMode, watchModeAlpha, watchModeBeta synchronization.WatchMode
   264  	if createConfiguration.watchMode != "" {
   265  		if err := watchMode.UnmarshalText([]byte(createConfiguration.watchMode)); err != nil {
   266  			return fmt.Errorf("unable to parse watch mode: %w", err)
   267  		}
   268  	}
   269  	if createConfiguration.watchModeAlpha != "" {
   270  		if err := watchModeAlpha.UnmarshalText([]byte(createConfiguration.watchModeAlpha)); err != nil {
   271  			return fmt.Errorf("unable to parse watch mode for alpha: %w", err)
   272  		}
   273  	}
   274  	if createConfiguration.watchModeBeta != "" {
   275  		if err := watchModeBeta.UnmarshalText([]byte(createConfiguration.watchModeBeta)); err != nil {
   276  			return fmt.Errorf("unable to parse watch mode for beta: %w", err)
   277  		}
   278  	}
   279  
   280  	// There's no need to validate the watch polling intervals - any uint32
   281  	// values are valid.
   282  
   283  	// Validate and convert the ignore syntax specification.
   284  	var ignoreSyntax ignore.Syntax
   285  	if createConfiguration.ignoreSyntax != "" {
   286  		if err := ignoreSyntax.UnmarshalText([]byte(createConfiguration.ignoreSyntax)); err != nil {
   287  			return fmt.Errorf("unable to parse ignore syntax: %w", err)
   288  		}
   289  	}
   290  
   291  	// Unfortunately we can't validate ignore specifications in any meaningful
   292  	// way because we don't yet know the ignore syntax being used. This could be
   293  	// specified by the global YAML configuration or (more likely) determined by
   294  	// the default session version within the daemon. These ignores will
   295  	// eventually be validated at endpoint initialization time, but there's no
   296  	// convenient way to do it earlier in the session creation process.
   297  
   298  	// Validate and convert the VCS ignore mode specification.
   299  	var ignoreVCSMode ignore.IgnoreVCSMode
   300  	if createConfiguration.ignoreVCS && createConfiguration.noIgnoreVCS {
   301  		return errors.New("conflicting VCS ignore behavior specified")
   302  	} else if createConfiguration.ignoreVCS {
   303  		ignoreVCSMode = ignore.IgnoreVCSMode_IgnoreVCSModeIgnore
   304  	} else if createConfiguration.noIgnoreVCS {
   305  		ignoreVCSMode = ignore.IgnoreVCSMode_IgnoreVCSModePropagate
   306  	}
   307  
   308  	// Validate and convert the permissions mode specification.
   309  	var permissionsMode core.PermissionsMode
   310  	if createConfiguration.permissionsMode != "" {
   311  		if err := permissionsMode.UnmarshalText([]byte(createConfiguration.permissionsMode)); err != nil {
   312  			return fmt.Errorf("unable to parse permissions mode: %w", err)
   313  		}
   314  	}
   315  
   316  	// Compute the effective permissions mode.
   317  	// HACK: We technically don't know the daemon's default session version, so
   318  	// we compute the default permissions mode using the default session version
   319  	// for this executable, which (given our current distribution strategy) will
   320  	// be the same as that of the daemon. Of course, the daemon API will
   321  	// re-validate this, so validation here is merely best-effort and
   322  	// informational in any case. For more information on the reasoning behind
   323  	// this, see the note in synchronization.Version.DefaultPermissionsMode.
   324  	effectivePermissionsMode := permissionsMode
   325  	if effectivePermissionsMode.IsDefault() {
   326  		effectivePermissionsMode = synchronization.DefaultVersion.DefaultPermissionsMode()
   327  	}
   328  
   329  	// Validate and convert default file mode specifications.
   330  	var defaultFileMode, defaultFileModeAlpha, defaultFileModeBeta filesystem.Mode
   331  	if createConfiguration.defaultFileMode != "" {
   332  		if err := defaultFileMode.UnmarshalText([]byte(createConfiguration.defaultFileMode)); err != nil {
   333  			return fmt.Errorf("unable to parse default file mode: %w", err)
   334  		} else if err = core.EnsureDefaultFileModeValid(effectivePermissionsMode, defaultFileMode); err != nil {
   335  			return fmt.Errorf("invalid default file mode: %w", err)
   336  		}
   337  	}
   338  	if createConfiguration.defaultFileModeAlpha != "" {
   339  		if err := defaultFileModeAlpha.UnmarshalText([]byte(createConfiguration.defaultFileModeAlpha)); err != nil {
   340  			return fmt.Errorf("unable to parse default file mode for alpha: %w", err)
   341  		} else if err = core.EnsureDefaultFileModeValid(effectivePermissionsMode, defaultFileModeAlpha); err != nil {
   342  			return fmt.Errorf("invalid default file mode for alpha: %w", err)
   343  		}
   344  	}
   345  	if createConfiguration.defaultFileModeBeta != "" {
   346  		if err := defaultFileModeBeta.UnmarshalText([]byte(createConfiguration.defaultFileModeBeta)); err != nil {
   347  			return fmt.Errorf("unable to parse default file mode for beta: %w", err)
   348  		} else if err = core.EnsureDefaultFileModeValid(effectivePermissionsMode, defaultFileModeBeta); err != nil {
   349  			return fmt.Errorf("invalid default file mode for beta: %w", err)
   350  		}
   351  	}
   352  
   353  	// Validate and convert default directory mode specifications.
   354  	var defaultDirectoryMode, defaultDirectoryModeAlpha, defaultDirectoryModeBeta filesystem.Mode
   355  	if createConfiguration.defaultDirectoryMode != "" {
   356  		if err := defaultDirectoryMode.UnmarshalText([]byte(createConfiguration.defaultDirectoryMode)); err != nil {
   357  			return fmt.Errorf("unable to parse default directory mode: %w", err)
   358  		} else if err = core.EnsureDefaultDirectoryModeValid(effectivePermissionsMode, defaultDirectoryMode); err != nil {
   359  			return fmt.Errorf("invalid default directory mode: %w", err)
   360  		}
   361  	}
   362  	if createConfiguration.defaultDirectoryModeAlpha != "" {
   363  		if err := defaultDirectoryModeAlpha.UnmarshalText([]byte(createConfiguration.defaultDirectoryModeAlpha)); err != nil {
   364  			return fmt.Errorf("unable to parse default directory mode for alpha: %w", err)
   365  		} else if err = core.EnsureDefaultDirectoryModeValid(effectivePermissionsMode, defaultDirectoryModeAlpha); err != nil {
   366  			return fmt.Errorf("invalid default directory mode for alpha: %w", err)
   367  		}
   368  	}
   369  	if createConfiguration.defaultDirectoryModeBeta != "" {
   370  		if err := defaultDirectoryModeBeta.UnmarshalText([]byte(createConfiguration.defaultDirectoryModeBeta)); err != nil {
   371  			return fmt.Errorf("unable to parse default directory mode for beta: %w", err)
   372  		} else if err = core.EnsureDefaultDirectoryModeValid(effectivePermissionsMode, defaultDirectoryModeBeta); err != nil {
   373  			return fmt.Errorf("invalid default directory mode for beta: %w", err)
   374  		}
   375  	}
   376  
   377  	// Validate default file owner specifications.
   378  	if createConfiguration.defaultOwner != "" {
   379  		if kind, _ := filesystem.ParseOwnershipIdentifier(
   380  			createConfiguration.defaultOwner,
   381  		); kind == filesystem.OwnershipIdentifierKindInvalid {
   382  			return errors.New("invalid ownership specification")
   383  		}
   384  	}
   385  	if createConfiguration.defaultOwnerAlpha != "" {
   386  		if kind, _ := filesystem.ParseOwnershipIdentifier(
   387  			createConfiguration.defaultOwnerAlpha,
   388  		); kind == filesystem.OwnershipIdentifierKindInvalid {
   389  			return errors.New("invalid ownership specification for alpha")
   390  		}
   391  	}
   392  	if createConfiguration.defaultOwnerBeta != "" {
   393  		if kind, _ := filesystem.ParseOwnershipIdentifier(
   394  			createConfiguration.defaultOwnerBeta,
   395  		); kind == filesystem.OwnershipIdentifierKindInvalid {
   396  			return errors.New("invalid ownership specification for beta")
   397  		}
   398  	}
   399  
   400  	// Validate default file group specifications.
   401  	if createConfiguration.defaultGroup != "" {
   402  		if kind, _ := filesystem.ParseOwnershipIdentifier(
   403  			createConfiguration.defaultGroup,
   404  		); kind == filesystem.OwnershipIdentifierKindInvalid {
   405  			return errors.New("invalid group specification")
   406  		}
   407  	}
   408  	if createConfiguration.defaultGroupAlpha != "" {
   409  		if kind, _ := filesystem.ParseOwnershipIdentifier(
   410  			createConfiguration.defaultGroupAlpha,
   411  		); kind == filesystem.OwnershipIdentifierKindInvalid {
   412  			return errors.New("invalid group specification for alpha")
   413  		}
   414  	}
   415  	if createConfiguration.defaultGroupBeta != "" {
   416  		if kind, _ := filesystem.ParseOwnershipIdentifier(
   417  			createConfiguration.defaultGroupBeta,
   418  		); kind == filesystem.OwnershipIdentifierKindInvalid {
   419  			return errors.New("invalid group specification for beta")
   420  		}
   421  	}
   422  
   423  	// Validate and convert compression algorithm specifications.
   424  	var compressionAlgorithm, compressionAlgorithmAlpha, compressionAlgorithmBeta compression.Algorithm
   425  	if createConfiguration.compression != "" {
   426  		if err := compressionAlgorithm.UnmarshalText([]byte(createConfiguration.compression)); err != nil {
   427  			return fmt.Errorf("unable to parse compression algorithm: %w", err)
   428  		}
   429  	}
   430  	if createConfiguration.compressionAlpha != "" {
   431  		if err := compressionAlgorithmAlpha.UnmarshalText([]byte(createConfiguration.compressionAlpha)); err != nil {
   432  			return fmt.Errorf("unable to parse compression algorithm for alpha: %w", err)
   433  		}
   434  	}
   435  	if createConfiguration.compressionBeta != "" {
   436  		if err := compressionAlgorithmBeta.UnmarshalText([]byte(createConfiguration.compressionBeta)); err != nil {
   437  			return fmt.Errorf("unable to parse compression algorithm for beta: %w", err)
   438  		}
   439  	}
   440  
   441  	// Create the command line configuration and merge it into our cumulative
   442  	// configuration.
   443  	configuration = synchronization.MergeConfigurations(configuration, &synchronization.Configuration{
   444  		SynchronizationMode:    synchronizationMode,
   445  		HashingAlgorithm:       hashingAlgorithm,
   446  		MaximumEntryCount:      createConfiguration.maximumEntryCount,
   447  		MaximumStagingFileSize: maximumStagingFileSize,
   448  		ProbeMode:              probeMode,
   449  		ScanMode:               scanMode,
   450  		StageMode:              stageMode,
   451  		SymbolicLinkMode:       symbolicLinkMode,
   452  		WatchMode:              watchMode,
   453  		WatchPollingInterval:   createConfiguration.watchPollingInterval,
   454  		IgnoreSyntax:           ignoreSyntax,
   455  		Ignores:                createConfiguration.ignores,
   456  		IgnoreVCSMode:          ignoreVCSMode,
   457  		PermissionsMode:        permissionsMode,
   458  		DefaultFileMode:        uint32(defaultFileMode),
   459  		DefaultDirectoryMode:   uint32(defaultDirectoryMode),
   460  		DefaultOwner:           createConfiguration.defaultOwner,
   461  		DefaultGroup:           createConfiguration.defaultGroup,
   462  		CompressionAlgorithm:   compressionAlgorithm,
   463  	})
   464  
   465  	// Create the creation specification.
   466  	specification := &synchronizationsvc.CreationSpecification{
   467  		Alpha:         alpha,
   468  		Beta:          beta,
   469  		Configuration: configuration,
   470  		ConfigurationAlpha: &synchronization.Configuration{
   471  			ProbeMode:            probeModeAlpha,
   472  			ScanMode:             scanModeAlpha,
   473  			StageMode:            stageModeAlpha,
   474  			WatchMode:            watchModeAlpha,
   475  			WatchPollingInterval: createConfiguration.watchPollingIntervalAlpha,
   476  			DefaultFileMode:      uint32(defaultFileModeAlpha),
   477  			DefaultDirectoryMode: uint32(defaultDirectoryModeAlpha),
   478  			DefaultOwner:         createConfiguration.defaultOwnerAlpha,
   479  			DefaultGroup:         createConfiguration.defaultGroupAlpha,
   480  			CompressionAlgorithm: compressionAlgorithmAlpha,
   481  		},
   482  		ConfigurationBeta: &synchronization.Configuration{
   483  			ProbeMode:            probeModeBeta,
   484  			ScanMode:             scanModeBeta,
   485  			StageMode:            stageModeBeta,
   486  			WatchMode:            watchModeBeta,
   487  			WatchPollingInterval: createConfiguration.watchPollingIntervalBeta,
   488  			DefaultFileMode:      uint32(defaultFileModeBeta),
   489  			DefaultDirectoryMode: uint32(defaultDirectoryModeBeta),
   490  			DefaultOwner:         createConfiguration.defaultOwnerBeta,
   491  			DefaultGroup:         createConfiguration.defaultGroupBeta,
   492  			CompressionAlgorithm: compressionAlgorithmBeta,
   493  		},
   494  		Name:   createConfiguration.name,
   495  		Labels: labels,
   496  		Paused: createConfiguration.paused,
   497  	}
   498  
   499  	// Connect to the daemon and defer closure of the connection.
   500  	daemonConnection, err := daemon.Connect(true, true)
   501  	if err != nil {
   502  		return fmt.Errorf("unable to connect to daemon: %w", err)
   503  	}
   504  	defer daemonConnection.Close()
   505  
   506  	// Perform the create operation.
   507  	identifier, err := CreateWithSpecification(daemonConnection, specification)
   508  	if err != nil {
   509  		return err
   510  	}
   511  
   512  	// Print the session identifier.
   513  	fmt.Println("Created session", identifier)
   514  
   515  	// Success.
   516  	return nil
   517  }
   518  
   519  // createCommand is the create command.
   520  var createCommand = &cobra.Command{
   521  	Use:          "create <alpha> <beta>",
   522  	Short:        "Create and start a new synchronization session",
   523  	RunE:         createMain,
   524  	SilenceUsage: true,
   525  }
   526  
   527  // createConfiguration stores configuration for the create command.
   528  var createConfiguration struct {
   529  	// help indicates whether or not to show help information and exit.
   530  	help bool
   531  	// name is the name specification for the session.
   532  	name string
   533  	// labels are the label specifications for the session.
   534  	labels []string
   535  	// paused indicates whether or not to create the session in a pre-paused
   536  	// state.
   537  	paused bool
   538  	// noGlobalConfiguration specifies whether or not the global configuration
   539  	// file should be ignored.
   540  	noGlobalConfiguration bool
   541  	// configurationFiles stores paths of additional files from which to load
   542  	// default configuration.
   543  	configurationFiles []string
   544  	// synchronizationMode specifies the synchronization mode for the session.
   545  	synchronizationMode string
   546  	// hash specifies the hashing algorithm to use for the session.
   547  	hash string
   548  	// maximumEntryCount specifies the maximum number of filesystem entries that
   549  	// endpoints will tolerate managing.
   550  	maximumEntryCount uint64
   551  	// maximumStagingFileSize is the maximum file size that endpoints will
   552  	// stage. It can be specified in human-friendly units.
   553  	maximumStagingFileSize string
   554  	// probeMode specifies the filesystem probing mode to use for the session.
   555  	probeMode string
   556  	// probeModeAlpha specifies the filesystem probing mode to use for the
   557  	// session, taking priority over probeMode on alpha if specified.
   558  	probeModeAlpha string
   559  	// probeModeBeta specifies the filesystem probing mode to use for the
   560  	// session, taking priority over probeMode on beta if specified.
   561  	probeModeBeta string
   562  	// scanMode specifies the scan mode to use for the session.
   563  	scanMode string
   564  	// scanModeAlpha specifies the scan mode to use for the session, taking
   565  	// priority over scanMode on alpha if specified.
   566  	scanModeAlpha string
   567  	// scanModeBeta specifies the scan mode to use for the session, taking
   568  	// priority over scanMode on beta if specified.
   569  	scanModeBeta string
   570  	// stageMode specifies the file staging mode to use for the session.
   571  	stageMode string
   572  	// stageModeAlpha specifies the file staging mode to use for the session,
   573  	// taking priority over stageMode on alpha if specified.
   574  	stageModeAlpha string
   575  	// stageModeBeta specifies the file staging mode to use for the session,
   576  	// taking priority over stageMode on beta if specified.
   577  	stageModeBeta string
   578  	// symbolicLinkMode specifies the symbolic link handling mode to use for
   579  	// the session.
   580  	symbolicLinkMode string
   581  	// watchMode specifies the filesystem watching mode to use for the session.
   582  	watchMode string
   583  	// watchModeAlpha specifies the filesystem watching mode to use for the
   584  	// session, taking priority over watchMode on alpha if specified.
   585  	watchModeAlpha string
   586  	// watchModeBeta specifies the filesystem watching mode to use for the
   587  	// session, taking priority over watchMode on beta if specified.
   588  	watchModeBeta string
   589  	// watchPollingInterval specifies the polling interval to use if using
   590  	// poll-based or hybrid watching.
   591  	watchPollingInterval uint32
   592  	// watchPollingIntervalAlpha specifies the polling interval to use if using
   593  	// poll-based or hybrid watching, taking priority over watchPollingInterval
   594  	// on alpha if specified.
   595  	watchPollingIntervalAlpha uint32
   596  	// watchPollingIntervalBeta specifies the polling interval to use if using
   597  	// poll-based or hybrid watching, taking priority over watchPollingInterval
   598  	// on beta if specified.
   599  	watchPollingIntervalBeta uint32
   600  	// ignoreSyntax specifies the ignore syntax and semantics for the session.
   601  	ignoreSyntax string
   602  	// ignores is the list of ignore specifications for the session.
   603  	ignores []string
   604  	// ignoreVCS specifies whether or not to enable VCS ignores for the session.
   605  	ignoreVCS bool
   606  	// noIgnoreVCS specifies whether or not to disable VCS ignores for the
   607  	// session.
   608  	noIgnoreVCS bool
   609  	// permissionsMode specifies the permissions mode to use for the session.
   610  	permissionsMode string
   611  	// defaultFileMode specifies the default permission mode to use for new
   612  	// files in "portable" permission propagation mode, with endpoint-specific
   613  	// specifications taking priority.
   614  	defaultFileMode string
   615  	// defaultFileModeAlpha specifies the default permission mode to use for new
   616  	// files on alpha in "portable" permission propagation mode, taking priority
   617  	// over defaultFileMode on alpha if specified.
   618  	defaultFileModeAlpha string
   619  	// defaultFileModeBeta specifies the default permission mode to use for new
   620  	// files on beta in "portable" permission propagation mode, taking priority
   621  	// over defaultFileMode on beta if specified.
   622  	defaultFileModeBeta string
   623  	// defaultDirectoryMode specifies the default permission mode to use for new
   624  	// directories in "portable" permission propagation mode, with endpoint-
   625  	// specific specifications taking priority.
   626  	defaultDirectoryMode string
   627  	// defaultDirectoryModeAlpha specifies the default permission mode to use
   628  	// for new directories on alpha in "portable" permission propagation mode,
   629  	// taking priority over defaultDirectoryMode on alpha if specified.
   630  	defaultDirectoryModeAlpha string
   631  	// defaultDirectoryModeBeta specifies the default permission mode to use for
   632  	// new directories on beta in "portable" permission propagation mode, taking
   633  	// priority over defaultDirectoryMode on beta if specified.
   634  	defaultDirectoryModeBeta string
   635  	// defaultOwner specifies the default owner identifier to use when setting
   636  	// ownership of new files and directories in "portable" permission
   637  	// propagation mode, with endpoint-specific specifications taking priority.
   638  	defaultOwner string
   639  	// defaultOwnerAlpha specifies the default owner identifier to use when
   640  	// setting ownership of new files and directories on alpha in "portable"
   641  	// permission propagation mode, taking priority over defaultOwner on alpha
   642  	// if specified.
   643  	defaultOwnerAlpha string
   644  	// defaultOwnerBeta specifies the default owner identifier to use when
   645  	// setting ownership of new files and directories on beta in "portable"
   646  	// permission propagation mode, taking priority over defaultOwner on beta if
   647  	// specified.
   648  	defaultOwnerBeta string
   649  	// defaultGroup specifies the default group identifier to use when setting
   650  	// ownership of new files and directories in "portable" permission
   651  	// propagation mode, with endpoint-specific specifications taking priority.
   652  	defaultGroup string
   653  	// defaultGroupAlpha specifies the default group identifier to use when
   654  	// setting ownership of new files and directories on alpha in "portable"
   655  	// permission propagation mode, taking priority over defaultGroup on alpha
   656  	// if specified.
   657  	defaultGroupAlpha string
   658  	// defaultGroupBeta specifies the default group identifier to use when
   659  	// setting ownership of new files and directories on beta in "portable"
   660  	// permission propagation mode, taking priority over defaultGroup on beta if
   661  	// specified.
   662  	defaultGroupBeta string
   663  	// compression specifies the compression algorithm to use when communicating
   664  	// with remote endpoints.
   665  	compression string
   666  	// compressionAlpha specifies the compression algorithm to use when
   667  	// communicating with a remote alpha endpoint.
   668  	compressionAlpha string
   669  	// compressionBeta specifies the compression algorithm to use when
   670  	// communicating with a remote beta endpoint.
   671  	compressionBeta string
   672  }
   673  
   674  func init() {
   675  	// Grab a handle for the command line flags.
   676  	flags := createCommand.Flags()
   677  
   678  	// Disable alphabetical sorting of flags in help output.
   679  	flags.SortFlags = false
   680  
   681  	// Manually add a help flag to override the default message. Cobra will
   682  	// still implement its logic automatically.
   683  	flags.BoolVarP(&createConfiguration.help, "help", "h", false, "Show help information")
   684  
   685  	// Wire up name and label flags.
   686  	flags.StringVarP(&createConfiguration.name, "name", "n", "", "Specify a name for the session")
   687  	flags.StringSliceVarP(&createConfiguration.labels, "label", "l", nil, "Specify labels")
   688  
   689  	// Wire up paused flags.
   690  	flags.BoolVarP(&createConfiguration.paused, "paused", "p", false, "Create the session pre-paused")
   691  
   692  	// Wire up general configuration flags.
   693  	flags.BoolVar(&createConfiguration.noGlobalConfiguration, "no-global-configuration", false, "Ignore the global configuration file")
   694  	flags.StringSliceVarP(&createConfiguration.configurationFiles, "configuration-file", "c", nil, "Specify additional files from which to load (and merge) default configuration parameters")
   695  
   696  	// Wire up synchronization flags.
   697  	flags.StringVarP(&createConfiguration.synchronizationMode, "mode", "m", "", "Specify synchronization mode (two-way-safe|two-way-resolved|one-way-safe|one-way-replica)")
   698  	flags.StringVarP(&createConfiguration.hash, "hash", "H", "", "Specify content hashing algorithm ("+hashFlagOptions+")")
   699  	flags.Uint64Var(&createConfiguration.maximumEntryCount, "max-entry-count", 0, "Specify the maximum number of entries that endpoints will manage")
   700  	flags.StringVar(&createConfiguration.maximumStagingFileSize, "max-staging-file-size", "", "Specify the maximum (individual) file size that endpoints will stage")
   701  	flags.StringVar(&createConfiguration.probeMode, "probe-mode", "", "Specify probe mode (probe|assume)")
   702  	flags.StringVar(&createConfiguration.probeModeAlpha, "probe-mode-alpha", "", "Specify probe mode for alpha (probe|assume)")
   703  	flags.StringVar(&createConfiguration.probeModeBeta, "probe-mode-beta", "", "Specify probe mode for beta (probe|assume)")
   704  	flags.StringVar(&createConfiguration.scanMode, "scan-mode", "", "Specify scan mode (full|accelerated)")
   705  	flags.StringVar(&createConfiguration.scanModeAlpha, "scan-mode-alpha", "", "Specify scan mode for alpha (full|accelerated)")
   706  	flags.StringVar(&createConfiguration.scanModeBeta, "scan-mode-beta", "", "Specify scan mode for beta (full|accelerated)")
   707  	flags.StringVar(&createConfiguration.stageMode, "stage-mode", "", "Specify staging mode (mutagen|neighboring)")
   708  	flags.StringVar(&createConfiguration.stageModeAlpha, "stage-mode-alpha", "", "Specify staging mode for alpha (mutagen|neighboring)")
   709  	flags.StringVar(&createConfiguration.stageModeBeta, "stage-mode-beta", "", "Specify staging mode for beta (mutagen|neighboring)")
   710  
   711  	// Wire up symbolic link flags.
   712  	flags.StringVar(&createConfiguration.symbolicLinkMode, "symlink-mode", "", "Specify symlink mode (ignore|portable|posix-raw)")
   713  
   714  	// Wire up watch flags.
   715  	flags.StringVar(&createConfiguration.watchMode, "watch-mode", "", "Specify watch mode (portable|force-poll|no-watch)")
   716  	flags.StringVar(&createConfiguration.watchModeAlpha, "watch-mode-alpha", "", "Specify watch mode for alpha (portable|force-poll|no-watch)")
   717  	flags.StringVar(&createConfiguration.watchModeBeta, "watch-mode-beta", "", "Specify watch mode for beta (portable|force-poll|no-watch)")
   718  	flags.Uint32Var(&createConfiguration.watchPollingInterval, "watch-polling-interval", 0, "Specify watch polling interval in seconds")
   719  	flags.Uint32Var(&createConfiguration.watchPollingIntervalAlpha, "watch-polling-interval-alpha", 0, "Specify watch polling interval in seconds for alpha")
   720  	flags.Uint32Var(&createConfiguration.watchPollingIntervalBeta, "watch-polling-interval-beta", 0, "Specify watch polling interval in seconds for beta")
   721  
   722  	// Wire up ignore flags.
   723  	flags.StringVar(&createConfiguration.ignoreSyntax, "ignore-syntax", "", "Specify ignore syntax (mutagen|docker)")
   724  	flags.StringSliceVarP(&createConfiguration.ignores, "ignore", "i", nil, "Specify ignore paths")
   725  	flags.BoolVar(&createConfiguration.ignoreVCS, "ignore-vcs", false, "Ignore VCS directories")
   726  	flags.BoolVar(&createConfiguration.noIgnoreVCS, "no-ignore-vcs", false, "Propagate VCS directories")
   727  
   728  	// Wire up permission flags.
   729  	flags.StringVar(&createConfiguration.permissionsMode, "permissions-mode", "", "Specify permissions mode (portable|manual)")
   730  	flags.StringVar(&createConfiguration.defaultFileMode, "default-file-mode", "", "Specify default file permission mode")
   731  	flags.StringVar(&createConfiguration.defaultFileModeAlpha, "default-file-mode-alpha", "", "Specify default file permission mode for alpha")
   732  	flags.StringVar(&createConfiguration.defaultFileModeBeta, "default-file-mode-beta", "", "Specify default file permission mode for beta")
   733  	flags.StringVar(&createConfiguration.defaultDirectoryMode, "default-directory-mode", "", "Specify default directory permission mode")
   734  	flags.StringVar(&createConfiguration.defaultDirectoryModeAlpha, "default-directory-mode-alpha", "", "Specify default directory permission mode for alpha")
   735  	flags.StringVar(&createConfiguration.defaultDirectoryModeBeta, "default-directory-mode-beta", "", "Specify default directory permission mode for beta")
   736  	flags.StringVar(&createConfiguration.defaultOwner, "default-owner", "", "Specify default file/directory owner")
   737  	flags.StringVar(&createConfiguration.defaultOwnerAlpha, "default-owner-alpha", "", "Specify default file/directory owner for alpha")
   738  	flags.StringVar(&createConfiguration.defaultOwnerBeta, "default-owner-beta", "", "Specify default file/directory owner for beta")
   739  	flags.StringVar(&createConfiguration.defaultGroup, "default-group", "", "Specify default file/directory group")
   740  	flags.StringVar(&createConfiguration.defaultGroupAlpha, "default-group-alpha", "", "Specify default file/directory group for alpha")
   741  	flags.StringVar(&createConfiguration.defaultGroupBeta, "default-group-beta", "", "Specify default file/directory group for beta")
   742  
   743  	// Wire up compression flags.
   744  	flags.StringVarP(&createConfiguration.compression, "compression", "C", "", "Specify compression algorithm ("+compressionFlagOptions+")")
   745  	flags.StringVar(&createConfiguration.compressionAlpha, "compression-alpha", "", "Specify compression algorithm for alpha ("+compressionFlagOptions+")")
   746  	flags.StringVar(&createConfiguration.compressionBeta, "compression-beta", "", "Specify compression algorithm for beta ("+compressionFlagOptions+")")
   747  
   748  	// Set up flag normalization. This is only required to handle aliases.
   749  	flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
   750  		if name == "sync-mode" {
   751  			name = "mode"
   752  		}
   753  		return pflag.NormalizedName(name)
   754  	})
   755  }