github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/forward/create.go (about) 1 package forward 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "strings" 9 10 "github.com/spf13/cobra" 11 12 "google.golang.org/grpc" 13 14 "github.com/mutagen-io/mutagen/cmd" 15 "github.com/mutagen-io/mutagen/cmd/mutagen/daemon" 16 17 "github.com/mutagen-io/mutagen/pkg/configuration/global" 18 "github.com/mutagen-io/mutagen/pkg/filesystem" 19 "github.com/mutagen-io/mutagen/pkg/forwarding" 20 "github.com/mutagen-io/mutagen/pkg/grpcutil" 21 "github.com/mutagen-io/mutagen/pkg/selection" 22 forwardingsvc "github.com/mutagen-io/mutagen/pkg/service/forwarding" 23 promptingsvc "github.com/mutagen-io/mutagen/pkg/service/prompting" 24 "github.com/mutagen-io/mutagen/pkg/url" 25 ) 26 27 // loadAndValidateGlobalSynchronizationConfiguration loads a YAML-based global 28 // configuration, extracts the forwarding component, converts it to a Protocol 29 // Buffers session configuration, and validates it. 30 func loadAndValidateGlobalForwardingConfiguration(path string) (*forwarding.Configuration, error) { 31 // Load the YAML configuration. 32 yamlConfiguration, err := global.LoadConfiguration(path) 33 if err != nil { 34 return nil, err 35 } 36 37 // Convert the YAML configuration to a Protocol Buffers representation and 38 // validate it. 39 configuration := yamlConfiguration.Forwarding.Defaults.ToInternal() 40 if err := configuration.EnsureValid(false); err != nil { 41 return nil, fmt.Errorf("invalid configuration: %w", err) 42 } 43 44 // Success. 45 return configuration, nil 46 } 47 48 // CreateWithSpecification is an orchestration convenience method that performs 49 // a create operation using the provided daemon connection and session 50 // specification. 51 func CreateWithSpecification( 52 daemonConnection *grpc.ClientConn, 53 specification *forwardingsvc.CreationSpecification, 54 ) (string, error) { 55 // Initiate command line prompting. 56 statusLinePrinter := &cmd.StatusLinePrinter{} 57 promptingCtx, promptingCancel := context.WithCancel(context.Background()) 58 prompter, promptingErrors, err := promptingsvc.Host( 59 promptingCtx, promptingsvc.NewPromptingClient(daemonConnection), 60 &cmd.StatusLinePrompter{Printer: statusLinePrinter}, true, 61 ) 62 if err != nil { 63 promptingCancel() 64 return "", fmt.Errorf("unable to initiate prompting: %w", err) 65 } 66 67 // Perform the create operation, cancel prompting, and handle errors. 68 forwardingService := forwardingsvc.NewForwardingClient(daemonConnection) 69 request := &forwardingsvc.CreateRequest{ 70 Prompter: prompter, 71 Specification: specification, 72 } 73 response, err := forwardingService.Create(context.Background(), request) 74 promptingCancel() 75 <-promptingErrors 76 if err != nil { 77 statusLinePrinter.BreakIfPopulated() 78 return "", grpcutil.PeelAwayRPCErrorLayer(err) 79 } else if err = response.EnsureValid(); err != nil { 80 statusLinePrinter.BreakIfPopulated() 81 return "", fmt.Errorf("invalid create response received: %w", err) 82 } 83 84 // Success. 85 statusLinePrinter.Clear() 86 return response.Session, nil 87 } 88 89 // createMain is the entry point for the create command. 90 func createMain(_ *cobra.Command, arguments []string) error { 91 // Validate, extract, and parse URLs. 92 if len(arguments) != 2 { 93 return errors.New("invalid number of endpoint URLs provided") 94 } 95 source, err := url.Parse(arguments[0], url.Kind_Forwarding, true) 96 if err != nil { 97 return fmt.Errorf("unable to parse source URL: %w", err) 98 } 99 destination, err := url.Parse(arguments[1], url.Kind_Forwarding, false) 100 if err != nil { 101 return fmt.Errorf("unable to parse destination URL: %w", err) 102 } 103 104 // Validate the name. 105 if err := selection.EnsureNameValid(createConfiguration.name); err != nil { 106 return fmt.Errorf("invalid session name: %w", err) 107 } 108 109 // Parse, validate, and record labels. 110 var labels map[string]string 111 if len(createConfiguration.labels) > 0 { 112 labels = make(map[string]string, len(createConfiguration.labels)) 113 } 114 for _, label := range createConfiguration.labels { 115 components := strings.SplitN(label, "=", 2) 116 var key, value string 117 key = components[0] 118 if len(components) == 2 { 119 value = components[1] 120 } 121 if err := selection.EnsureLabelKeyValid(key); err != nil { 122 return fmt.Errorf("invalid label key: %w", err) 123 } else if err := selection.EnsureLabelValueValid(value); err != nil { 124 return fmt.Errorf("invalid label value: %w", err) 125 } 126 labels[key] = value 127 } 128 129 // Create a default session configuration that will form the basis of our 130 // cumulative configuration. 131 configuration := &forwarding.Configuration{} 132 133 // Unless disabled, attempt to load configuration from the global 134 // configuration file and merge it into our cumulative configuration. 135 if !createConfiguration.noGlobalConfiguration { 136 // Compute the path to the global configuration file. 137 globalConfigurationPath, err := global.ConfigurationPath() 138 if err != nil { 139 return fmt.Errorf("unable to compute path to global configuration file: %w", err) 140 } 141 142 // Attempt to load the file. We allow it to not exist. 143 globalConfiguration, err := loadAndValidateGlobalForwardingConfiguration(globalConfigurationPath) 144 if err != nil { 145 if !os.IsNotExist(err) { 146 return fmt.Errorf("unable to load global configuration: %w", err) 147 } 148 } else { 149 configuration = forwarding.MergeConfigurations(configuration, globalConfiguration) 150 } 151 } 152 153 // If additional default configuration files have been specified, then load 154 // them and merge them into the cumulative configuration. 155 for _, configurationFile := range createConfiguration.configurationFiles { 156 if c, err := loadAndValidateGlobalForwardingConfiguration(configurationFile); err != nil { 157 return fmt.Errorf("unable to load configuration file (%s): %w", configurationFile, err) 158 } else { 159 configuration = forwarding.MergeConfigurations(configuration, c) 160 } 161 } 162 163 // Validate and convert socket overwrite mode specifications. 164 var socketOverwriteMode, socketOverwriteModeSource, socketOverwriteModeDestination forwarding.SocketOverwriteMode 165 if createConfiguration.socketOverwriteMode != "" { 166 if err := socketOverwriteMode.UnmarshalText([]byte(createConfiguration.socketOverwriteMode)); err != nil { 167 return fmt.Errorf("unable to socket overwrite mode: %w", err) 168 } 169 } 170 if createConfiguration.socketOverwriteModeSource != "" { 171 if err := socketOverwriteModeSource.UnmarshalText([]byte(createConfiguration.socketOverwriteModeSource)); err != nil { 172 return fmt.Errorf("unable to socket overwrite mode for source: %w", err) 173 } 174 } 175 if createConfiguration.socketOverwriteModeDestination != "" { 176 if err := socketOverwriteModeDestination.UnmarshalText([]byte(createConfiguration.socketOverwriteModeDestination)); err != nil { 177 return fmt.Errorf("unable to socket overwrite mode for destination: %w", err) 178 } 179 } 180 181 // Validate socket owner specifications. 182 if createConfiguration.socketOwner != "" { 183 if kind, _ := filesystem.ParseOwnershipIdentifier( 184 createConfiguration.socketOwner, 185 ); kind == filesystem.OwnershipIdentifierKindInvalid { 186 return errors.New("invalid socket ownership specification") 187 } 188 } 189 if createConfiguration.socketOwnerSource != "" { 190 if kind, _ := filesystem.ParseOwnershipIdentifier( 191 createConfiguration.socketOwnerSource, 192 ); kind == filesystem.OwnershipIdentifierKindInvalid { 193 return errors.New("invalid socket ownership specification for source") 194 } 195 } 196 if createConfiguration.socketOwnerDestination != "" { 197 if kind, _ := filesystem.ParseOwnershipIdentifier( 198 createConfiguration.socketOwnerDestination, 199 ); kind == filesystem.OwnershipIdentifierKindInvalid { 200 return errors.New("invalid socket ownership specification for destination") 201 } 202 } 203 204 // Validate socket group specifications. 205 if createConfiguration.socketGroup != "" { 206 if kind, _ := filesystem.ParseOwnershipIdentifier( 207 createConfiguration.socketGroup, 208 ); kind == filesystem.OwnershipIdentifierKindInvalid { 209 return errors.New("invalid socket group specification") 210 } 211 } 212 if createConfiguration.socketGroupSource != "" { 213 if kind, _ := filesystem.ParseOwnershipIdentifier( 214 createConfiguration.socketGroupSource, 215 ); kind == filesystem.OwnershipIdentifierKindInvalid { 216 return errors.New("invalid socket group specification for source") 217 } 218 } 219 if createConfiguration.socketGroupDestination != "" { 220 if kind, _ := filesystem.ParseOwnershipIdentifier( 221 createConfiguration.socketGroupDestination, 222 ); kind == filesystem.OwnershipIdentifierKindInvalid { 223 return errors.New("invalid socket group specification for destination") 224 } 225 } 226 227 // Validate and convert socket permission mode specifications. 228 var socketPermissionMode, socketPermissionModeSource, socketPermissionModeDestination filesystem.Mode 229 if createConfiguration.socketPermissionMode != "" { 230 if err := socketPermissionMode.UnmarshalText([]byte(createConfiguration.socketPermissionMode)); err != nil { 231 return fmt.Errorf("unable to parse socket permission mode: %w", err) 232 } 233 } 234 if createConfiguration.socketPermissionModeSource != "" { 235 if err := socketPermissionModeSource.UnmarshalText([]byte(createConfiguration.socketPermissionModeSource)); err != nil { 236 return fmt.Errorf("unable to parse socket permission mode for source: %w", err) 237 } 238 } 239 if createConfiguration.socketPermissionModeDestination != "" { 240 if err := socketPermissionModeDestination.UnmarshalText([]byte(createConfiguration.socketPermissionModeDestination)); err != nil { 241 return fmt.Errorf("unable to parse socket permission mode for destination: %w", err) 242 } 243 } 244 245 // Create the command line configuration and merge it into our cumulative 246 // configuration. 247 configuration = forwarding.MergeConfigurations(configuration, &forwarding.Configuration{ 248 SocketOverwriteMode: socketOverwriteMode, 249 SocketOwner: createConfiguration.socketOwner, 250 SocketGroup: createConfiguration.socketGroup, 251 SocketPermissionMode: uint32(socketPermissionMode), 252 }) 253 254 // Create the creation specification. 255 specification := &forwardingsvc.CreationSpecification{ 256 Source: source, 257 Destination: destination, 258 Configuration: configuration, 259 ConfigurationSource: &forwarding.Configuration{ 260 SocketOverwriteMode: socketOverwriteModeSource, 261 SocketOwner: createConfiguration.socketOwnerSource, 262 SocketGroup: createConfiguration.socketGroupSource, 263 SocketPermissionMode: uint32(socketPermissionModeSource), 264 }, 265 ConfigurationDestination: &forwarding.Configuration{ 266 SocketOverwriteMode: socketOverwriteModeDestination, 267 SocketOwner: createConfiguration.socketOwnerDestination, 268 SocketGroup: createConfiguration.socketGroupDestination, 269 SocketPermissionMode: uint32(socketPermissionModeDestination), 270 }, 271 Name: createConfiguration.name, 272 Labels: labels, 273 Paused: createConfiguration.paused, 274 } 275 276 // Connect to the daemon and defer closure of the connection. 277 daemonConnection, err := daemon.Connect(true, true) 278 if err != nil { 279 return fmt.Errorf("unable to connect to daemon: %w", err) 280 } 281 defer daemonConnection.Close() 282 283 // Perform the create operation. 284 identifier, err := CreateWithSpecification(daemonConnection, specification) 285 if err != nil { 286 return err 287 } 288 289 // Print the session identifier. 290 fmt.Println("Created session", identifier) 291 292 // Success. 293 return nil 294 } 295 296 // createCommand is the create command. 297 var createCommand = &cobra.Command{ 298 Use: "create <source> <destination>", 299 Short: "Create and start a new forwarding session", 300 RunE: createMain, 301 SilenceUsage: true, 302 } 303 304 // createConfiguration stores configuration for the create command. 305 var createConfiguration struct { 306 // help indicates whether or not to show help information and exit. 307 help bool 308 // name is the name specification for the session. 309 name string 310 // labels are the label specifications for the session. 311 labels []string 312 // paused indicates whether or not to create the session in a pre-paused 313 // state. 314 paused bool 315 // noGlobalConfiguration specifies whether or not the global configuration 316 // file should be ignored. 317 noGlobalConfiguration bool 318 // configurationFiles stores paths of additional files from which to load 319 // default configuration. 320 configurationFiles []string 321 // socketOverwriteMode specifies the socket overwrite mode to use for the 322 // session. 323 socketOverwriteMode string 324 // socketOverwriteModeSource specifies the socket overwrite mode to use for 325 // the session, taking priority over socketOverwriteMode on source if 326 // specified. 327 socketOverwriteModeSource string 328 // socketOverwriteModeDestination specifies the socket overwrite mode to use 329 // for the session, taking priority over socketOverwriteMode on destination 330 // if specified. 331 socketOverwriteModeDestination string 332 // socketOwner specifies the socket owner identifier to use new Unix domain 333 // socket listeners, with endpoint-specific specifications taking priority. 334 socketOwner string 335 // socketOwnerSource specifies the socket owner identifier to use new Unix 336 // domain socket listeners, taking priority over socketOwner on source if 337 // specified. 338 socketOwnerSource string 339 // socketOwnerDestination specifies the socket owner identifier to use new 340 // Unix domain socket listeners, taking priority over socketOwner on 341 // destination if specified. 342 socketOwnerDestination string 343 // socketGroup specifies the socket owner identifier to use new Unix domain 344 // socket listeners, with endpoint-specific specifications taking priority. 345 socketGroup string 346 // socketGroupSource specifies the socket owner identifier to use new Unix 347 // domain socket listeners, taking priority over socketGroup on source if 348 // specified. 349 socketGroupSource string 350 // socketGroupDestination specifies the socket owner identifier to use new 351 // Unix domain socket listeners, taking priority over socketGroup on 352 // destination if specified. 353 socketGroupDestination string 354 // socketPermissionMode specifies the socket permission mode to use for new 355 // Unix domain socket listeners, with endpoint-specific specifications 356 // taking priority. 357 socketPermissionMode string 358 // socketPermissionModeSource specifies the socket permission mode to use 359 // for new Unix domain socket listeners on source, taking priority over 360 // socketPermissionMode on source if specified. 361 socketPermissionModeSource string 362 // socketPermissionModeDestination specifies the socket permission mode to 363 // use for new Unix domain socket listeners on destination, taking priority 364 // over socketPermissionMode on destination if specified. 365 socketPermissionModeDestination string 366 } 367 368 func init() { 369 // Grab a handle for the command line flags. 370 flags := createCommand.Flags() 371 372 // Disable alphabetical sorting of flags in help output. 373 flags.SortFlags = false 374 375 // Manually add a help flag to override the default message. Cobra will 376 // still implement its logic automatically. 377 flags.BoolVarP(&createConfiguration.help, "help", "h", false, "Show help information") 378 379 // Wire up name and label flags. 380 flags.StringVarP(&createConfiguration.name, "name", "n", "", "Specify a name for the session") 381 flags.StringSliceVarP(&createConfiguration.labels, "label", "l", nil, "Specify labels") 382 383 // Wire up paused flags. 384 flags.BoolVarP(&createConfiguration.paused, "paused", "p", false, "Create the session pre-paused") 385 386 // Wire up general configuration flags. 387 flags.BoolVar(&createConfiguration.noGlobalConfiguration, "no-global-configuration", false, "Ignore the global configuration file") 388 flags.StringSliceVarP(&createConfiguration.configurationFiles, "configuration-file", "c", nil, "Specify additional files from which to load (and merge) default configuration parameters") 389 390 // Wire up socket flags. 391 flags.StringVar(&createConfiguration.socketOverwriteMode, "socket-overwrite-mode", "", "Specify socket overwrite mode (leave|overwrite)") 392 flags.StringVar(&createConfiguration.socketOverwriteModeSource, "socket-overwrite-mode-source", "", "Specify socket overwrite mode for source (leave|overwrite)") 393 flags.StringVar(&createConfiguration.socketOverwriteModeDestination, "socket-overwrite-mode-destination", "", "Specify socket overwrite mode for destination (leave|overwrite)") 394 flags.StringVar(&createConfiguration.socketOwner, "socket-owner", "", "Specify socket owner") 395 flags.StringVar(&createConfiguration.socketOwnerSource, "socket-owner-source", "", "Specify socket owner for source") 396 flags.StringVar(&createConfiguration.socketOwnerDestination, "socket-owner-destination", "", "Specify socket owner for destination") 397 flags.StringVar(&createConfiguration.socketGroup, "socket-group", "", "Specify socket group") 398 flags.StringVar(&createConfiguration.socketGroupSource, "socket-group-source", "", "Specify socket group for source") 399 flags.StringVar(&createConfiguration.socketGroupDestination, "socket-group-destination", "", "Specify socket group for destination") 400 flags.StringVar(&createConfiguration.socketPermissionMode, "socket-permission-mode", "", "Specify socket permission mode") 401 flags.StringVar(&createConfiguration.socketPermissionModeSource, "socket-permission-mode-source", "", "Specify socket permission mode for source") 402 flags.StringVar(&createConfiguration.socketPermissionModeDestination, "socket-permission-mode-destination", "", "Specify socket permission mode for destination") 403 }