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

     1  package project
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  
    11  	"github.com/spf13/cobra"
    12  
    13  	"github.com/mutagen-io/mutagen/cmd"
    14  	"github.com/mutagen-io/mutagen/cmd/mutagen/daemon"
    15  	"github.com/mutagen-io/mutagen/cmd/mutagen/forward"
    16  	"github.com/mutagen-io/mutagen/cmd/mutagen/sync"
    17  
    18  	"github.com/mutagen-io/mutagen/pkg/configuration/global"
    19  	"github.com/mutagen-io/mutagen/pkg/filesystem/locking"
    20  	"github.com/mutagen-io/mutagen/pkg/forwarding"
    21  	"github.com/mutagen-io/mutagen/pkg/identifier"
    22  	"github.com/mutagen-io/mutagen/pkg/project"
    23  	"github.com/mutagen-io/mutagen/pkg/selection"
    24  	forwardingsvc "github.com/mutagen-io/mutagen/pkg/service/forwarding"
    25  	synchronizationsvc "github.com/mutagen-io/mutagen/pkg/service/synchronization"
    26  	"github.com/mutagen-io/mutagen/pkg/synchronization"
    27  	"github.com/mutagen-io/mutagen/pkg/url"
    28  )
    29  
    30  // startMain is the entry point for the start command.
    31  func startMain(_ *cobra.Command, _ []string) error {
    32  	// Compute the name of the configuration file and ensure that our working
    33  	// directory is that in which the file resides. This is required for
    34  	// relative paths (including relative synchronization paths and relative
    35  	// Unix Domain Socket paths) to be resolved relative to the project
    36  	// configuration file.
    37  	configurationFileName := project.DefaultConfigurationFileName
    38  	if startConfiguration.projectFile != "" {
    39  		var directory string
    40  		directory, configurationFileName = filepath.Split(startConfiguration.projectFile)
    41  		if directory != "" {
    42  			if err := os.Chdir(directory); err != nil {
    43  				return fmt.Errorf("unable to switch to target directory: %w", err)
    44  			}
    45  		}
    46  	}
    47  
    48  	// Compute the lock path.
    49  	lockPath := configurationFileName + project.LockFileExtension
    50  
    51  	// Track whether or not we should remove the lock file on return.
    52  	var removeLockFileOnReturn bool
    53  
    54  	// Create a locker and defer its closure and potential removal. On Windows
    55  	// systems, we have to handle this removal after the file is closed.
    56  	locker, err := locking.NewLocker(lockPath, 0600)
    57  	if err != nil {
    58  		return fmt.Errorf("unable to create project locker: %w", err)
    59  	}
    60  	defer func() {
    61  		locker.Close()
    62  		if removeLockFileOnReturn && runtime.GOOS == "windows" {
    63  			os.Remove(lockPath)
    64  		}
    65  	}()
    66  
    67  	// Acquire the project lock and defer its release and potential removal. On
    68  	// Windows systems, we can't remove the lock file if it's locked or even
    69  	// just opened, so we handle removal for Windows systems after we close the
    70  	// lock file (see above). In this case, we truncate the lock file before
    71  	// releasing it to ensure that any other process that opens or acquires the
    72  	// lock file before we manage to remove it will simply see an empty lock
    73  	// file, which it will ignore or attempt to remove.
    74  	if err := locker.Lock(true); err != nil {
    75  		return fmt.Errorf("unable to acquire project lock: %w", err)
    76  	}
    77  	defer func() {
    78  		if removeLockFileOnReturn {
    79  			if runtime.GOOS == "windows" {
    80  				locker.Truncate(0)
    81  			} else {
    82  				os.Remove(lockPath)
    83  			}
    84  		}
    85  		locker.Unlock()
    86  	}()
    87  
    88  	// Read the full contents of the lock file and ensure that it's empty.
    89  	buffer := &bytes.Buffer{}
    90  	if length, err := buffer.ReadFrom(locker); err != nil {
    91  		return fmt.Errorf("unable to read project lock: %w", err)
    92  	} else if length != 0 {
    93  		return errors.New("project already running")
    94  	}
    95  
    96  	// At this point we know that there was no previous project running, but we
    97  	// haven't yet created any resources, so defer removal of the lock file that
    98  	// we've created in case we run into any errors loading configuration
    99  	// information.
   100  	removeLockFileOnReturn = true
   101  
   102  	// Create a unique project identifier.
   103  	identifier, err := identifier.New(identifier.PrefixProject)
   104  	if err != nil {
   105  		return fmt.Errorf("unable to generate project identifier: %w", err)
   106  	}
   107  
   108  	// Write the project identifier to the lock file.
   109  	if _, err := locker.Write([]byte(identifier)); err != nil {
   110  		return fmt.Errorf("unable to write project identifier: %w", err)
   111  	}
   112  
   113  	// Load the configuration file.
   114  	configuration, err := project.LoadConfiguration(configurationFileName)
   115  	if err != nil {
   116  		return fmt.Errorf("unable to load configuration file: %w", err)
   117  	}
   118  
   119  	// Unless disabled, attempt to load configuration from the global
   120  	// configuration file and use it as the base for our core session
   121  	// configurations.
   122  	globalConfigurationForwarding := &forwarding.Configuration{}
   123  	globalConfigurationSynchronization := &synchronization.Configuration{}
   124  	if !startConfiguration.noGlobalConfiguration {
   125  		// Compute the path to the global configuration file.
   126  		globalConfigurationPath, err := global.ConfigurationPath()
   127  		if err != nil {
   128  			return fmt.Errorf("unable to compute path to global configuration file: %w", err)
   129  		}
   130  
   131  		// Attempt to load and validate the file. We allow it to not exist.
   132  		globalConfiguration, err := global.LoadConfiguration(globalConfigurationPath)
   133  		if err != nil {
   134  			if !os.IsNotExist(err) {
   135  				return fmt.Errorf("unable to load global configuration: %w", err)
   136  			}
   137  		} else {
   138  			globalConfigurationForwarding = globalConfiguration.Forwarding.Defaults.ToInternal()
   139  			if err := globalConfigurationForwarding.EnsureValid(false); err != nil {
   140  				return fmt.Errorf("invalid global forwarding configuration: %w", err)
   141  			}
   142  			globalConfigurationSynchronization = globalConfiguration.Synchronization.Defaults.ToInternal()
   143  			if err := globalConfigurationSynchronization.EnsureValid(false); err != nil {
   144  				return fmt.Errorf("invalid global synchronization configuration: %w", err)
   145  			}
   146  		}
   147  	}
   148  
   149  	// Extract and validate forwarding defaults.
   150  	var defaultSource, defaultDestination string
   151  	defaultConfigurationForwarding := &forwarding.Configuration{}
   152  	defaultConfigurationSource := &forwarding.Configuration{}
   153  	defaultConfigurationDestination := &forwarding.Configuration{}
   154  	if defaults, ok := configuration.Forwarding["defaults"]; ok {
   155  		defaultSource = defaults.Source
   156  		defaultDestination = defaults.Destination
   157  		defaultConfigurationForwarding = defaults.Configuration.ToInternal()
   158  		if err := defaultConfigurationForwarding.EnsureValid(false); err != nil {
   159  			return fmt.Errorf("invalid default forwarding configuration: %w", err)
   160  		}
   161  		defaultConfigurationSource = defaults.ConfigurationSource.ToInternal()
   162  		if err := defaultConfigurationSource.EnsureValid(true); err != nil {
   163  			return fmt.Errorf("invalid default forwarding source configuration: %w", err)
   164  		}
   165  		defaultConfigurationDestination = defaults.ConfigurationDestination.ToInternal()
   166  		if err := defaultConfigurationDestination.EnsureValid(true); err != nil {
   167  			return fmt.Errorf("invalid default forwarding destination configuration: %w", err)
   168  		}
   169  	}
   170  
   171  	// Extract and validate synchronization defaults.
   172  	var defaultAlpha, defaultBeta string
   173  	var defaultFlushOnCreate project.FlushOnCreateBehavior
   174  	defaultConfigurationSynchronization := &synchronization.Configuration{}
   175  	defaultConfigurationAlpha := &synchronization.Configuration{}
   176  	defaultConfigurationBeta := &synchronization.Configuration{}
   177  	if defaults, ok := configuration.Synchronization["defaults"]; ok {
   178  		defaultAlpha = defaults.Alpha
   179  		defaultBeta = defaults.Beta
   180  		defaultFlushOnCreate = defaults.FlushOnCreate
   181  		defaultConfigurationSynchronization = defaults.Configuration.ToInternal()
   182  		if err := defaultConfigurationSynchronization.EnsureValid(false); err != nil {
   183  			return fmt.Errorf("invalid default synchronization configuration: %w", err)
   184  		}
   185  		defaultConfigurationAlpha = defaults.ConfigurationAlpha.ToInternal()
   186  		if err := defaultConfigurationAlpha.EnsureValid(true); err != nil {
   187  			return fmt.Errorf("invalid default synchronization alpha configuration: %w", err)
   188  		}
   189  		defaultConfigurationBeta = defaults.ConfigurationBeta.ToInternal()
   190  		if err := defaultConfigurationBeta.EnsureValid(true); err != nil {
   191  			return fmt.Errorf("invalid default synchronization beta configuration: %w", err)
   192  		}
   193  	}
   194  
   195  	// Merge global and default configurations, with defaults taking priority.
   196  	defaultConfigurationForwarding = forwarding.MergeConfigurations(
   197  		globalConfigurationForwarding,
   198  		defaultConfigurationForwarding,
   199  	)
   200  	defaultConfigurationSynchronization = synchronization.MergeConfigurations(
   201  		globalConfigurationSynchronization,
   202  		defaultConfigurationSynchronization,
   203  	)
   204  
   205  	// Generate forward session creation specifications.
   206  	var forwardingSpecifications []*forwardingsvc.CreationSpecification
   207  	for name, session := range configuration.Forwarding {
   208  		// Ignore defaults.
   209  		if name == "defaults" {
   210  			continue
   211  		}
   212  
   213  		// Verify that the name is valid.
   214  		if err := selection.EnsureNameValid(name); err != nil {
   215  			return fmt.Errorf("invalid forwarding session name (%s): %v", name, err)
   216  		}
   217  
   218  		// Compute URLs.
   219  		source := session.Source
   220  		if source == "" {
   221  			source = defaultSource
   222  		}
   223  		destination := session.Destination
   224  		if destination == "" {
   225  			destination = defaultDestination
   226  		}
   227  
   228  		// Parse URLs.
   229  		sourceURL, err := url.Parse(source, url.Kind_Forwarding, true)
   230  		if err != nil {
   231  			return fmt.Errorf("unable to parse forwarding source URL (%s): %v", source, err)
   232  		}
   233  		destinationURL, err := url.Parse(destination, url.Kind_Forwarding, false)
   234  		if err != nil {
   235  			return fmt.Errorf("unable to parse forwarding destination URL (%s): %v", destination, err)
   236  		}
   237  
   238  		// Compute configuration.
   239  		configuration := session.Configuration.ToInternal()
   240  		if err := configuration.EnsureValid(false); err != nil {
   241  			return fmt.Errorf("invalid forwarding session configuration for %s: %v", name, err)
   242  		}
   243  		configuration = forwarding.MergeConfigurations(defaultConfigurationForwarding, configuration)
   244  
   245  		// Compute source-specific configuration.
   246  		sourceConfiguration := session.ConfigurationSource.ToInternal()
   247  		if err := sourceConfiguration.EnsureValid(true); err != nil {
   248  			return fmt.Errorf("invalid forwarding session source configuration for %s: %v", name, err)
   249  		}
   250  		sourceConfiguration = forwarding.MergeConfigurations(defaultConfigurationSource, sourceConfiguration)
   251  
   252  		// Compute destination-specific configuration.
   253  		destinationConfiguration := session.ConfigurationDestination.ToInternal()
   254  		if err := destinationConfiguration.EnsureValid(true); err != nil {
   255  			return fmt.Errorf("invalid forwarding session destination configuration for %s: %v", name, err)
   256  		}
   257  		destinationConfiguration = forwarding.MergeConfigurations(defaultConfigurationDestination, destinationConfiguration)
   258  
   259  		// Record the specification.
   260  		forwardingSpecifications = append(forwardingSpecifications, &forwardingsvc.CreationSpecification{
   261  			Source:                   sourceURL,
   262  			Destination:              destinationURL,
   263  			Configuration:            configuration,
   264  			ConfigurationSource:      sourceConfiguration,
   265  			ConfigurationDestination: destinationConfiguration,
   266  			Name:                     name,
   267  			Labels: map[string]string{
   268  				project.LabelKey: identifier,
   269  			},
   270  			Paused: startConfiguration.paused,
   271  		})
   272  	}
   273  
   274  	// Generate synchronization session creation specifications and keep track
   275  	// of those that we should flush on creation.
   276  	var synchronizationSpecifications []*synchronizationsvc.CreationSpecification
   277  	var flushOnCreateByIndex []bool
   278  	for name, session := range configuration.Synchronization {
   279  		// Ignore defaults.
   280  		if name == "defaults" {
   281  			continue
   282  		}
   283  
   284  		// Verify that the name is valid.
   285  		if err := selection.EnsureNameValid(name); err != nil {
   286  			return fmt.Errorf("invalid synchronization session name (%s): %v", name, err)
   287  		}
   288  
   289  		// Compute URLs.
   290  		alpha := session.Alpha
   291  		if alpha == "" {
   292  			alpha = defaultAlpha
   293  		}
   294  		beta := session.Beta
   295  		if beta == "" {
   296  			beta = defaultBeta
   297  		}
   298  
   299  		// Parse URLs.
   300  		alphaURL, err := url.Parse(alpha, url.Kind_Synchronization, true)
   301  		if err != nil {
   302  			return fmt.Errorf("unable to parse synchronization alpha URL (%s): %v", alpha, err)
   303  		}
   304  		betaURL, err := url.Parse(beta, url.Kind_Synchronization, false)
   305  		if err != nil {
   306  			return fmt.Errorf("unable to parse synchronization beta URL (%s): %v", beta, err)
   307  		}
   308  
   309  		// Compute configuration.
   310  		configuration := session.Configuration.ToInternal()
   311  		if err := configuration.EnsureValid(false); err != nil {
   312  			return fmt.Errorf("invalid synchronization session configuration for %s: %v", name, err)
   313  		}
   314  		configuration = synchronization.MergeConfigurations(defaultConfigurationSynchronization, configuration)
   315  
   316  		// Compute alpha-specific configuration.
   317  		alphaConfiguration := session.ConfigurationAlpha.ToInternal()
   318  		if err := alphaConfiguration.EnsureValid(true); err != nil {
   319  			return fmt.Errorf("invalid synchronization session alpha configuration for %s: %v", name, err)
   320  		}
   321  		alphaConfiguration = synchronization.MergeConfigurations(defaultConfigurationAlpha, alphaConfiguration)
   322  
   323  		// Compute beta-specific configuration.
   324  		betaConfiguration := session.ConfigurationBeta.ToInternal()
   325  		if err := betaConfiguration.EnsureValid(true); err != nil {
   326  			return fmt.Errorf("invalid synchronization session beta configuration for %s: %v", name, err)
   327  		}
   328  		betaConfiguration = synchronization.MergeConfigurations(defaultConfigurationBeta, betaConfiguration)
   329  
   330  		// Record the specification.
   331  		synchronizationSpecifications = append(synchronizationSpecifications, &synchronizationsvc.CreationSpecification{
   332  			Alpha:              alphaURL,
   333  			Beta:               betaURL,
   334  			Configuration:      configuration,
   335  			ConfigurationAlpha: alphaConfiguration,
   336  			ConfigurationBeta:  betaConfiguration,
   337  			Name:               name,
   338  			Labels: map[string]string{
   339  				project.LabelKey: identifier,
   340  			},
   341  			Paused: startConfiguration.paused,
   342  		})
   343  
   344  		// Compute and store flush-on-creation behavior.
   345  		if session.FlushOnCreate.IsDefault() {
   346  			flushOnCreateByIndex = append(flushOnCreateByIndex, defaultFlushOnCreate.FlushOnCreate())
   347  		} else {
   348  			flushOnCreateByIndex = append(flushOnCreateByIndex, session.FlushOnCreate.FlushOnCreate())
   349  		}
   350  	}
   351  
   352  	// Connect to the daemon and defer closure of the connection.
   353  	daemonConnection, err := daemon.Connect(true, true)
   354  	if err != nil {
   355  		return fmt.Errorf("unable to connect to daemon: %w", err)
   356  	}
   357  	defer daemonConnection.Close()
   358  
   359  	// At this point, we're going to try to create resources, so we need to
   360  	// maintain the lock file in case even some of them are successful.
   361  	removeLockFileOnReturn = false
   362  
   363  	// Perform pre-creation commands.
   364  	for _, command := range configuration.BeforeCreate {
   365  		fmt.Println(">", command)
   366  		if err := runInShell(command); err != nil {
   367  			return fmt.Errorf("pre-create command failed: %w", err)
   368  		}
   369  	}
   370  
   371  	// Create forwarding sessions.
   372  	for _, specification := range forwardingSpecifications {
   373  		if _, err := forward.CreateWithSpecification(daemonConnection, specification); err != nil {
   374  			return fmt.Errorf("unable to create forwarding session (%s): %v", specification.Name, err)
   375  		}
   376  	}
   377  
   378  	// Create synchronization sessions and track those that we should flush.
   379  	var sessionsToFlush []string
   380  	for s, specification := range synchronizationSpecifications {
   381  		// Perform session creation.
   382  		session, err := sync.CreateWithSpecification(daemonConnection, specification)
   383  		if err != nil {
   384  			return fmt.Errorf("unable to create synchronization session (%s): %v", specification.Name, err)
   385  		}
   386  
   387  		// Determine whether or not to flush this session.
   388  		if !startConfiguration.paused && flushOnCreateByIndex[s] {
   389  			sessionsToFlush = append(sessionsToFlush, session)
   390  		}
   391  	}
   392  
   393  	// Flush synchronization sessions for which flushing has been requested.
   394  	if len(sessionsToFlush) > 0 {
   395  		flushSelection := &selection.Selection{Specifications: sessionsToFlush}
   396  		if err := sync.FlushWithSelection(daemonConnection, flushSelection, false); err != nil {
   397  			return fmt.Errorf("unable to flush synchronization session(s): %w", err)
   398  		}
   399  	}
   400  
   401  	// Perform post-creation commands.
   402  	for _, command := range configuration.AfterCreate {
   403  		fmt.Println(">", command)
   404  		if err := runInShell(command); err != nil {
   405  			return fmt.Errorf("post-create command failed: %w", err)
   406  		}
   407  	}
   408  
   409  	// Success.
   410  	return nil
   411  }
   412  
   413  // startCommand is the start command.
   414  var startCommand = &cobra.Command{
   415  	Use:          "start",
   416  	Short:        "Start project sessions",
   417  	Args:         cmd.DisallowArguments,
   418  	RunE:         startMain,
   419  	SilenceUsage: true,
   420  }
   421  
   422  // startConfiguration stores configuration for the start command.
   423  var startConfiguration struct {
   424  	// help indicates whether or not to show help information and exit.
   425  	help bool
   426  	// projectFile is the path to the project file, if non-default.
   427  	projectFile string
   428  	// paused indicates whether or not to create sessions in a pre-paused state.
   429  	paused bool
   430  	// noGlobalConfiguration specifies whether or not the global configuration
   431  	// file should be ignored.
   432  	noGlobalConfiguration bool
   433  }
   434  
   435  func init() {
   436  	// Grab a handle for the command line flags.
   437  	flags := startCommand.Flags()
   438  
   439  	// Disable alphabetical sorting of flags in help output.
   440  	flags.SortFlags = false
   441  
   442  	// Manually add a help flag to override the default message. Cobra will
   443  	// still implement its logic automatically.
   444  	flags.BoolVarP(&startConfiguration.help, "help", "h", false, "Show help information")
   445  
   446  	// Wire up project file flags.
   447  	flags.StringVarP(&startConfiguration.projectFile, "project-file", "f", "", "Specify project file")
   448  
   449  	// Wire up paused flags.
   450  	flags.BoolVarP(&startConfiguration.paused, "paused", "p", false, "Create the session pre-paused")
   451  
   452  	// Wire up general configuration flags.
   453  	flags.BoolVar(&startConfiguration.noGlobalConfiguration, "no-global-configuration", false, "Ignore the global configuration file")
   454  }