github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/cmd/mod.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/spf13/cobra"
     9  	"github.com/spf13/viper"
    10  	"github.com/turbot/go-kit/helpers"
    11  	"github.com/turbot/steampipe/pkg/cmdconfig"
    12  	"github.com/turbot/steampipe/pkg/constants"
    13  	"github.com/turbot/steampipe/pkg/error_helpers"
    14  	"github.com/turbot/steampipe/pkg/filepaths"
    15  	"github.com/turbot/steampipe/pkg/modinstaller"
    16  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    17  	"github.com/turbot/steampipe/pkg/steampipeconfig/parse"
    18  	"github.com/turbot/steampipe/pkg/utils"
    19  )
    20  
    21  // mod management commands
    22  func modCmd() *cobra.Command {
    23  	var cmd = &cobra.Command{
    24  		Use:   "mod [command]",
    25  		Args:  cobra.NoArgs,
    26  		Short: "Steampipe mod management",
    27  		Long: `Steampipe mod management.
    28  
    29  Mods enable you to run, build, and share dashboards, benchmarks and other resources.
    30  
    31  Find pre-built mods in the public registry at https://hub.steampipe.io.
    32  
    33  Examples:
    34  
    35      # Create a new mod in the current directory
    36      steampipe mod init
    37  
    38      # Install a mod
    39      steampipe mod install github.com/turbot/steampipe-mod-aws-compliance
    40      
    41      # Update a mod
    42      steampipe mod update github.com/turbot/steampipe-mod-aws-compliance
    43      
    44      # List installed mods
    45      steampipe mod list
    46      
    47      # Uninstall a mod
    48      steampipe mod uninstall github.com/turbot/steampipe-mod-aws-compliance
    49  	`,
    50  	}
    51  
    52  	cmd.AddCommand(modInstallCmd())
    53  	cmd.AddCommand(modUninstallCmd())
    54  	cmd.AddCommand(modUpdateCmd())
    55  	cmd.AddCommand(modListCmd())
    56  	cmd.AddCommand(modInitCmd())
    57  	cmd.Flags().BoolP(constants.ArgHelp, "h", false, "Help for mod")
    58  
    59  	cmdconfig.OnCmd(cmd).
    60  		AddModLocationFlag()
    61  
    62  	return cmd
    63  }
    64  
    65  // install
    66  func modInstallCmd() *cobra.Command {
    67  	var cmd = &cobra.Command{
    68  		Use:   "install",
    69  		Run:   runModInstallCmd,
    70  		Short: "Install one or more mods and their dependencies",
    71  		Long: `Install one or more mods and their dependencies.
    72  
    73  Mods provide an easy way to share Steampipe queries, controls, and benchmarks.
    74  Find mods using the public registry at hub.steampipe.io.
    75  
    76  Examples:
    77  
    78    # Install a mod(steampipe-mod-aws-compliance)
    79    steampipe mod install github.com/turbot/steampipe-mod-aws-compliance
    80  
    81    # Install a specific version of a mod
    82    steampipe mod install github.com/turbot/steampipe-mod-aws-compliance@0.1
    83  
    84    # Install a version of a mod using a semver constraint
    85    steampipe mod install github.com/turbot/steampipe-mod-aws-compliance@'^1'
    86  
    87    # Install all mods specified in the mod.sp and their dependencies
    88    steampipe mod install
    89  
    90    # Preview what steampipe mod install will do, without actually installing anything
    91    steampipe mod install --dry-run`,
    92  	}
    93  
    94  	cmdconfig.OnCmd(cmd).
    95  		AddBoolFlag(constants.ArgPrune, true, "Remove unused dependencies after installation is complete").
    96  		AddBoolFlag(constants.ArgDryRun, false, "Show which mods would be installed/updated/uninstalled without modifying them").
    97  		AddBoolFlag(constants.ArgForce, false, "Install mods even if plugin/cli version requirements are not met (cannot be used with --dry-run)").
    98  		AddBoolFlag(constants.ArgHelp, false, "Help for install", cmdconfig.FlagOptions.WithShortHand("h")).
    99  		AddModLocationFlag()
   100  
   101  	return cmd
   102  }
   103  
   104  func runModInstallCmd(cmd *cobra.Command, args []string) {
   105  	ctx := cmd.Context()
   106  	utils.LogTime("cmd.runModInstallCmd")
   107  	defer func() {
   108  		utils.LogTime("cmd.runModInstallCmd end")
   109  		if r := recover(); r != nil {
   110  			error_helpers.ShowError(ctx, helpers.ToError(r))
   111  			exitCode = constants.ExitCodeUnknownErrorPanic
   112  		}
   113  	}()
   114  
   115  	// try to load the workspace mod definition
   116  	// - if it does not exist, this will return a nil mod and a nil error
   117  	workspacePath := viper.GetString(constants.ArgModLocation)
   118  	workspaceMod, err := parse.LoadModfile(workspacePath)
   119  	error_helpers.FailOnErrorWithMessage(err, "failed to load mod definition")
   120  
   121  	// if no mod was loaded, create a default
   122  	if workspaceMod == nil {
   123  		workspaceMod, err = createWorkspaceMod(ctx, cmd, workspacePath)
   124  		if err != nil {
   125  			exitCode = constants.ExitCodeModInstallFailed
   126  			error_helpers.FailOnError(err)
   127  		}
   128  	}
   129  
   130  	// if any mod names were passed as args, convert into formed mod names
   131  	opts := modinstaller.NewInstallOpts(workspaceMod, args...)
   132  	trimGitUrls(opts)
   133  	installData, err := modinstaller.InstallWorkspaceDependencies(ctx, opts)
   134  	if err != nil {
   135  		exitCode = constants.ExitCodeModInstallFailed
   136  		error_helpers.FailOnError(err)
   137  	}
   138  
   139  	fmt.Println(modinstaller.BuildInstallSummary(installData))
   140  }
   141  
   142  // uninstall
   143  func modUninstallCmd() *cobra.Command {
   144  	var cmd = &cobra.Command{
   145  		Use:   "uninstall",
   146  		Run:   runModUninstallCmd,
   147  		Short: "Uninstall a mod and its dependencies",
   148  		Long: `Uninstall a mod and its dependencies.
   149  
   150  Example:
   151    
   152    # Uninstall a mod
   153    steampipe mod uninstall github.com/turbot/steampipe-mod-azure-compliance`,
   154  	}
   155  
   156  	cmdconfig.OnCmd(cmd).
   157  		AddBoolFlag(constants.ArgPrune, true, "Remove unused dependencies after uninstallation is complete").
   158  		AddBoolFlag(constants.ArgDryRun, false, "Show which mods would be uninstalled without modifying them").
   159  		AddBoolFlag(constants.ArgHelp, false, "Help for uninstall", cmdconfig.FlagOptions.WithShortHand("h")).
   160  		AddModLocationFlag()
   161  
   162  	return cmd
   163  }
   164  
   165  func runModUninstallCmd(cmd *cobra.Command, args []string) {
   166  	ctx := cmd.Context()
   167  	utils.LogTime("cmd.runModInstallCmd")
   168  	defer func() {
   169  		utils.LogTime("cmd.runModInstallCmd end")
   170  		if r := recover(); r != nil {
   171  			error_helpers.ShowError(ctx, helpers.ToError(r))
   172  			exitCode = constants.ExitCodeUnknownErrorPanic
   173  		}
   174  	}()
   175  
   176  	// try to load the workspace mod definition
   177  	// - if it does not exist, this will return a nil mod and a nil error
   178  	workspaceMod, err := parse.LoadModfile(viper.GetString(constants.ArgModLocation))
   179  	error_helpers.FailOnErrorWithMessage(err, "failed to load mod definition")
   180  	if workspaceMod == nil {
   181  		fmt.Println("No mods installed.")
   182  		return
   183  	}
   184  	opts := modinstaller.NewInstallOpts(workspaceMod, args...)
   185  	trimGitUrls(opts)
   186  	installData, err := modinstaller.UninstallWorkspaceDependencies(ctx, opts)
   187  	error_helpers.FailOnError(err)
   188  
   189  	fmt.Println(modinstaller.BuildUninstallSummary(installData))
   190  }
   191  
   192  // update
   193  func modUpdateCmd() *cobra.Command {
   194  	var cmd = &cobra.Command{
   195  		Use:   "update",
   196  		Run:   runModUpdateCmd,
   197  		Short: "Update one or more mods and their dependencies",
   198  		Long: `Update one or more mods and their dependencies.
   199  
   200  Example:
   201  
   202    # Update a mod to the latest version allowed by its current constraint
   203    steampipe mod update github.com/turbot/steampipe-mod-aws-compliance
   204  
   205    # Update all mods specified in the mod.sp and their dependencies to the latest versions that meet their constraints, and install any that are missing
   206    steampipe mod update`,
   207  	}
   208  
   209  	cmdconfig.OnCmd(cmd).
   210  		AddBoolFlag(constants.ArgPrune, true, "Remove unused dependencies after update is complete").
   211  		AddBoolFlag(constants.ArgForce, false, "Update mods even if plugin/cli version requirements are not met (cannot be used with --dry-run)").
   212  		AddBoolFlag(constants.ArgDryRun, false, "Show which mods would be updated without modifying them").
   213  		AddBoolFlag(constants.ArgHelp, false, "Help for update", cmdconfig.FlagOptions.WithShortHand("h")).
   214  		AddModLocationFlag()
   215  
   216  	return cmd
   217  }
   218  
   219  func runModUpdateCmd(cmd *cobra.Command, args []string) {
   220  	ctx := cmd.Context()
   221  	utils.LogTime("cmd.runModUpdateCmd")
   222  	defer func() {
   223  		utils.LogTime("cmd.runModUpdateCmd end")
   224  		if r := recover(); r != nil {
   225  			error_helpers.ShowError(ctx, helpers.ToError(r))
   226  			exitCode = constants.ExitCodeUnknownErrorPanic
   227  		}
   228  	}()
   229  
   230  	// try to load the workspace mod definition
   231  	// - if it does not exist, this will return a nil mod and a nil error
   232  	workspaceMod, err := parse.LoadModfile(viper.GetString(constants.ArgModLocation))
   233  	error_helpers.FailOnErrorWithMessage(err, "failed to load mod definition")
   234  	if workspaceMod == nil {
   235  		fmt.Println("No mods installed.")
   236  		return
   237  	}
   238  
   239  	opts := modinstaller.NewInstallOpts(workspaceMod, args...)
   240  	trimGitUrls(opts)
   241  	installData, err := modinstaller.InstallWorkspaceDependencies(ctx, opts)
   242  	error_helpers.FailOnError(err)
   243  
   244  	fmt.Println(modinstaller.BuildInstallSummary(installData))
   245  }
   246  
   247  // list
   248  func modListCmd() *cobra.Command {
   249  	var cmd = &cobra.Command{
   250  		Use:   "list",
   251  		Run:   runModListCmd,
   252  		Short: "List currently installed mods",
   253  		Long: `List currently installed mods.
   254  		
   255  Example:
   256  
   257    # List installed mods
   258    steampipe mod list`,
   259  	}
   260  
   261  	cmdconfig.OnCmd(cmd).
   262  		AddBoolFlag(constants.ArgHelp, false, "Help for list", cmdconfig.FlagOptions.WithShortHand("h")).
   263  		AddModLocationFlag()
   264  	return cmd
   265  }
   266  
   267  func runModListCmd(cmd *cobra.Command, _ []string) {
   268  	ctx := cmd.Context()
   269  	utils.LogTime("cmd.runModListCmd")
   270  	defer func() {
   271  		utils.LogTime("cmd.runModListCmd end")
   272  		if r := recover(); r != nil {
   273  			error_helpers.ShowError(ctx, helpers.ToError(r))
   274  			exitCode = constants.ExitCodeUnknownErrorPanic
   275  		}
   276  	}()
   277  
   278  	// try to load the workspace mod definition
   279  	// - if it does not exist, this will return a nil mod and a nil error
   280  	workspaceMod, err := parse.LoadModfile(viper.GetString(constants.ArgModLocation))
   281  	error_helpers.FailOnErrorWithMessage(err, "failed to load mod definition")
   282  	if workspaceMod == nil {
   283  		fmt.Println("No mods installed.")
   284  		return
   285  	}
   286  
   287  	opts := modinstaller.NewInstallOpts(workspaceMod)
   288  	installer, err := modinstaller.NewModInstaller(ctx, opts)
   289  	error_helpers.FailOnError(err)
   290  
   291  	treeString := installer.GetModList()
   292  	if len(strings.Split(treeString, "\n")) > 1 {
   293  		fmt.Println()
   294  	}
   295  	fmt.Println(treeString)
   296  }
   297  
   298  // init
   299  func modInitCmd() *cobra.Command {
   300  	var cmd = &cobra.Command{
   301  		Use:   "init",
   302  		Run:   runModInitCmd,
   303  		Short: "Initialize the current directory with a mod.sp file",
   304  		Long: `Initialize the current directory with a mod.sp file.
   305  		
   306  Example:
   307  
   308    # Initialize the current directory with a mod.sp file
   309    steampipe mod init`,
   310  	}
   311  
   312  	cmdconfig.OnCmd(cmd).
   313  		AddBoolFlag(constants.ArgHelp, false, "Help for init", cmdconfig.FlagOptions.WithShortHand("h")).
   314  		AddModLocationFlag()
   315  	return cmd
   316  }
   317  
   318  func runModInitCmd(cmd *cobra.Command, args []string) {
   319  	utils.LogTime("cmd.runModInitCmd")
   320  	ctx := cmd.Context()
   321  
   322  	defer func() {
   323  		utils.LogTime("cmd.runModInitCmd end")
   324  		if r := recover(); r != nil {
   325  			error_helpers.ShowError(ctx, helpers.ToError(r))
   326  			exitCode = constants.ExitCodeUnknownErrorPanic
   327  		}
   328  	}()
   329  	workspacePath := viper.GetString(constants.ArgModLocation)
   330  	if _, err := createWorkspaceMod(ctx, cmd, workspacePath); err != nil {
   331  		exitCode = constants.ExitCodeModInitFailed
   332  		error_helpers.FailOnError(err)
   333  	}
   334  }
   335  
   336  // helpers
   337  func createWorkspaceMod(ctx context.Context, cmd *cobra.Command, workspacePath string) (*modconfig.Mod, error) {
   338  	cancel, err := modinstaller.ValidateModLocation(ctx, workspacePath)
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  	if !cancel {
   343  		return nil, fmt.Errorf("mod %s cancelled", cmd.Name())
   344  	}
   345  
   346  	if _, exists := parse.ModfileExists(workspacePath); exists {
   347  		fmt.Println("Working folder already contains a mod definition file")
   348  		return nil, nil
   349  	}
   350  	// write mod definition file
   351  	mod := modconfig.CreateDefaultMod(workspacePath)
   352  	if err := mod.Save(); err != nil {
   353  		return nil, err
   354  	}
   355  	// only print message for mod init (not for mod install)
   356  	if cmd.Name() == "init" {
   357  		fmt.Printf("Created mod definition file '%s'\n", filepaths.DefaultModFilePath(workspacePath))
   358  	}
   359  
   360  	// load up the written mod file so that we get the updated
   361  	// block ranges
   362  	mod, err = parse.LoadModfile(workspacePath)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  
   367  	return mod, nil
   368  }
   369  
   370  // Modifies(trims) the URL if contains http ot https in arguments
   371  func trimGitUrls(opts *modinstaller.InstallOpts) {
   372  	for i, url := range opts.ModArgs {
   373  		opts.ModArgs[i] = strings.TrimPrefix(url, "http://")
   374  		opts.ModArgs[i] = strings.TrimPrefix(url, "https://")
   375  	}
   376  }