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 }