get.porter.sh/porter@v1.3.0/cmd/porter/main.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"os"
     8  	"os/signal"
     9  	"runtime/debug"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"get.porter.sh/porter/pkg/cli"
    14  	"get.porter.sh/porter/pkg/config"
    15  	"get.porter.sh/porter/pkg/porter"
    16  	"github.com/spf13/cobra"
    17  	"github.com/spf13/pflag"
    18  	"go.opentelemetry.io/otel/attribute"
    19  	"go.opentelemetry.io/otel/trace"
    20  )
    21  
    22  var includeDocsCommand = false
    23  
    24  var includeGRPCServer string = "false"
    25  
    26  //go:embed helptext/usage.txt
    27  var usageText string
    28  
    29  const (
    30  	// Indicates that config should not be loaded for this command.
    31  	// This is used for commands like help and version which should never
    32  	// fail, even if porter is misconfigured.
    33  	skipConfig string = "skipConfig"
    34  )
    35  
    36  func main() {
    37  	run := func() int {
    38  		p := porter.New()
    39  		ctx, cancel := handleInterrupt(context.Background(), p)
    40  		defer cancel()
    41  
    42  		rootCmd := buildRootCommandFrom(p)
    43  
    44  		// Trace the command that called porter, e.g. porter installation show
    45  		cmd, commandName, formattedCommand := getCalledCommand(rootCmd)
    46  
    47  		// When running an internal plugin, switch how we log to be compatible
    48  		// with the hashicorp go-plugin framework
    49  		if commandName == "porter plugins run" {
    50  			p.IsInternalPlugin = true
    51  			if len(os.Args) > 3 {
    52  				p.InternalPluginKey = os.Args[3]
    53  			}
    54  		}
    55  
    56  		// Only run init logic that could fail for commands that
    57  		// really need it, skip it for commands that should NEVER
    58  		// fail.
    59  		if !shouldSkipConfig(cmd) {
    60  			var err error
    61  			ctx, err = p.Connect(ctx)
    62  			if err != nil {
    63  				fmt.Fprintln(os.Stderr, err.Error())
    64  				os.Exit(cli.ExitCodeErr)
    65  			}
    66  		}
    67  
    68  		ctx, log := p.StartRootSpan(ctx, commandName, attribute.String("command", formattedCommand))
    69  		defer func() {
    70  			// Capture panics and trace them
    71  			if panicErr := recover(); panicErr != nil {
    72  				_ = log.Error(fmt.Errorf("%s", panicErr),
    73  					attribute.Bool("panic", true),
    74  					attribute.String("stackTrace", string(debug.Stack())))
    75  				log.EndSpan()
    76  				p.Close()
    77  				os.Exit(cli.ExitCodeErr)
    78  			} else {
    79  				log.Close()
    80  				p.Close()
    81  			}
    82  		}()
    83  
    84  		if err := rootCmd.ExecuteContext(ctx); err != nil {
    85  			// Ideally we log all errors in the span that generated it,
    86  			// but as a failsafe, always log the error at the root span as well
    87  			_ = log.Error(err)
    88  			return cli.ExitCodeErr
    89  		}
    90  		return cli.ExitCodeSuccess
    91  	}
    92  
    93  	// Wrapping the main run logic in a function because os.Exit will not
    94  	// execute defer statements
    95  	os.Exit(run())
    96  }
    97  
    98  // Try to exit gracefully when the interrupt signal is sent (CTRL+C)
    99  // Thanks to Mat Ryer, https://pace.dev/blog/2020/02/17/repond-to-ctrl-c-interrupt-signals-gracefully-with-context-in-golang-by-mat-ryer.html
   100  func handleInterrupt(ctx context.Context, p *porter.Porter) (context.Context, func()) {
   101  	ctx, cancel := context.WithCancel(ctx)
   102  	signalChan := make(chan os.Signal, 1)
   103  	signal.Notify(signalChan, os.Interrupt)
   104  
   105  	go func() {
   106  		select {
   107  		case <-signalChan: // first signal, cancel context
   108  			fmt.Println("cancel requested", p.InternalPluginKey)
   109  			cancel()
   110  		case <-ctx.Done():
   111  		}
   112  		<-signalChan // second signal, hard exit
   113  		fmt.Println("hard interrupt received, bye!")
   114  		os.Exit(cli.ExitCodeInterrupt)
   115  	}()
   116  
   117  	return ctx, func() {
   118  		signal.Stop(signalChan)
   119  		cancel()
   120  	}
   121  }
   122  
   123  func shouldSkipConfig(cmd *cobra.Command) bool {
   124  	if cmd.Name() == "help" {
   125  		return true
   126  	}
   127  
   128  	_, skip := cmd.Annotations[skipConfig]
   129  	return skip
   130  }
   131  
   132  // Returns the porter command called, e.g. porter installation list
   133  // and also the fully formatted command as passed with arguments/flags.
   134  func getCalledCommand(cmd *cobra.Command) (*cobra.Command, string, string) {
   135  	// Ask cobra what sub-command was called, and walk up the tree to get the full command called.
   136  	var cmdChain []string
   137  	calledCommand, _, err := cmd.Find(os.Args[1:])
   138  	if err != nil {
   139  		cmdChain = append(cmdChain, "porter")
   140  	} else {
   141  		cmd := calledCommand
   142  		for cmd != nil {
   143  			cmdChain = append(cmdChain, cmd.Name())
   144  			cmd = cmd.Parent()
   145  		}
   146  	}
   147  	// reverse the command from [list installations porter] to porter installation list
   148  	var calledCommandBuilder strings.Builder
   149  	for i := len(cmdChain); i > 0; i-- {
   150  		calledCommandBuilder.WriteString(cmdChain[i-1])
   151  		calledCommandBuilder.WriteString(" ")
   152  	}
   153  	calledCommandStr := calledCommandBuilder.String()[0 : calledCommandBuilder.Len()-1]
   154  
   155  	// Also figure out the full command called, with args/flags.
   156  	formattedCommand := fmt.Sprintf("porter %s", strings.Join(os.Args[1:], " "))
   157  
   158  	return calledCommand, calledCommandStr, formattedCommand
   159  }
   160  
   161  func buildRootCommand() *cobra.Command {
   162  	return buildRootCommandFrom(porter.New())
   163  }
   164  
   165  func buildRootCommandFrom(p *porter.Porter) *cobra.Command {
   166  	var printVersion bool
   167  
   168  	cmd := &cobra.Command{
   169  		Use: "porter",
   170  		Short: `With Porter you can package your application artifact, client tools, configuration and deployment logic together as a versioned bundle that you can distribute, and then install with a single command.
   171  
   172  Most commands require a Docker daemon, either local or remote.
   173  
   174  Try our QuickStart https://porter.sh/quickstart to learn how to use Porter.
   175  `,
   176  		Example: `  porter create
   177    porter build
   178    porter install
   179    porter uninstall`,
   180  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
   181  			// Enable swapping out stdout/stderr for testing
   182  			p.Out = cmd.OutOrStdout()
   183  			p.Err = cmd.OutOrStderr()
   184  
   185  			if shouldSkipConfig(cmd) {
   186  				return nil
   187  			}
   188  
   189  			// Reload configuration with the now parsed cli flags
   190  			p.DataLoader = cli.LoadHierarchicalConfig(cmd)
   191  			ctx, err := p.Connect(cmd.Context())
   192  			// Extract the parent span from the main command
   193  			parentSpan := trace.SpanFromContext(cmd.Context())
   194  
   195  			// Create a context with the main command's span
   196  			ctxWithRootCmdSpan := trace.ContextWithSpan(ctx, parentSpan)
   197  
   198  			// Set the new context to the command
   199  			cmd.SetContext(ctxWithRootCmdSpan)
   200  			return err
   201  		},
   202  		RunE: func(cmd *cobra.Command, args []string) error {
   203  			if printVersion {
   204  				versionCmd := buildVersionCommand(p)
   205  				err := versionCmd.PreRunE(cmd, args)
   206  				if err != nil {
   207  					return err
   208  				}
   209  				return versionCmd.RunE(cmd, args)
   210  			}
   211  			return cmd.Help()
   212  		},
   213  		SilenceUsage:  true,
   214  		SilenceErrors: true, // Errors are printed by main
   215  	}
   216  
   217  	cmd.Annotations = map[string]string{
   218  		skipConfig: "",
   219  	}
   220  
   221  	// These flags are available for every command
   222  	globalFlags := cmd.PersistentFlags()
   223  	globalFlags.StringVar(&p.Data.Verbosity, "verbosity", config.DefaultVerbosity, "Threshold for printing messages to the console. Available values are: debug, info, warning, error.")
   224  	globalFlags.StringSliceVar(&p.Data.ExperimentalFlags, "experimental", nil, "Comma separated list of experimental features to enable. See https://porter.sh/configuration/#experimental-feature-flags for available feature flags.")
   225  
   226  	// Flags for just the porter command only, does not apply to sub-commands
   227  	cmd.Flags().BoolVarP(&printVersion, "version", "v", false, "Print the application version")
   228  
   229  	cmd.AddCommand(buildVersionCommand(p))
   230  	cmd.AddCommand(buildSchemaCommand(p))
   231  	cmd.AddCommand(buildStorageCommand(p))
   232  	cmd.AddCommand(buildRunCommand(p))
   233  	cmd.AddCommand(buildBundleCommands(p))
   234  	cmd.AddCommand(buildInstallationCommands(p))
   235  	cmd.AddCommand(buildMixinCommands(p))
   236  	cmd.AddCommand(buildPluginsCommands(p))
   237  	cmd.AddCommand(buildCredentialsCommands(p))
   238  	cmd.AddCommand(buildParametersCommands(p))
   239  	cmd.AddCommand(buildCompletionCommand(p))
   240  	//use -ldflags "-X main.includeGRPCServer=true" during build to include
   241  	grpcServer, _ := strconv.ParseBool(includeGRPCServer)
   242  	if grpcServer {
   243  		cmd.AddCommand(buildGRPCServerCommands(p))
   244  	}
   245  
   246  	for _, alias := range buildAliasCommands(p) {
   247  		cmd.AddCommand(alias)
   248  	}
   249  
   250  	cmd.SetUsageTemplate(usageText)
   251  	cobra.AddTemplateFunc("ShouldShowGroupCommands", ShouldShowGroupCommands)
   252  	cobra.AddTemplateFunc("ShouldShowGroupCommand", ShouldShowGroupCommand)
   253  	cobra.AddTemplateFunc("ShouldShowUngroupedCommands", ShouldShowUngroupedCommands)
   254  	cobra.AddTemplateFunc("ShouldShowUngroupedCommand", ShouldShowUngroupedCommand)
   255  
   256  	if includeDocsCommand {
   257  		cmd.AddCommand(buildDocsCommand(p))
   258  	}
   259  
   260  	return cmd
   261  }
   262  
   263  func ShouldShowGroupCommands(cmd *cobra.Command, group string) bool {
   264  	for _, child := range cmd.Commands() {
   265  		if ShouldShowGroupCommand(child, group) {
   266  			return true
   267  		}
   268  	}
   269  	return false
   270  }
   271  
   272  func ShouldShowGroupCommand(cmd *cobra.Command, group string) bool {
   273  	return cmd.Annotations["group"] == group
   274  }
   275  
   276  func ShouldShowUngroupedCommands(cmd *cobra.Command) bool {
   277  	for _, child := range cmd.Commands() {
   278  		if ShouldShowUngroupedCommand(child) {
   279  			return true
   280  		}
   281  	}
   282  	return false
   283  }
   284  
   285  func ShouldShowUngroupedCommand(cmd *cobra.Command) bool {
   286  	if !cmd.IsAvailableCommand() {
   287  		return false
   288  	}
   289  
   290  	_, hasGroup := cmd.Annotations["group"]
   291  	return !hasGroup
   292  }
   293  
   294  func addBundlePullFlags(f *pflag.FlagSet, opts *porter.BundlePullOptions) {
   295  	addReferenceFlag(f, opts)
   296  	addInsecureRegistryFlag(f, opts)
   297  	addForcePullFlag(f, opts)
   298  }
   299  
   300  func addReferenceFlag(f *pflag.FlagSet, opts *porter.BundlePullOptions) {
   301  	f.StringVarP(&opts.Reference, "reference", "r", "",
   302  		"Use a bundle in an OCI registry specified by the given reference.")
   303  }
   304  
   305  func addInsecureRegistryFlag(f *pflag.FlagSet, opts *porter.BundlePullOptions) {
   306  	f.BoolVar(&opts.InsecureRegistry, "insecure-registry", false,
   307  		"Don't require TLS for the registry")
   308  }
   309  
   310  func addForcePullFlag(f *pflag.FlagSet, opts *porter.BundlePullOptions) {
   311  	f.BoolVar(&opts.Force, "force", false,
   312  		"Force a fresh pull of the bundle")
   313  }
   314  
   315  func addBundleDefinitionFlags(f *pflag.FlagSet, opts *porter.BundleDefinitionOptions) {
   316  	f.StringVarP(&opts.File, "file", "f", "", "Path to the Porter manifest. Defaults to `porter.yaml` in the current directory.")
   317  	f.StringVar(&opts.CNABFile, "cnab-file", "", "Path to the CNAB bundle.json file.")
   318  	f.BoolVar(&opts.AutoBuildDisabled, "autobuild-disabled", false, "Do not automatically build the bundle from source when the last build is out-of-date.")
   319  }