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  }