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 }