Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
var CleanupCmd = &cobra.Command{ Use: "cleanup", Short: "Clean up old extension images and containers to free disk space", Long: `Remove old versions of extension Docker images and stopped containers to reclaim disk space. By default, keeps only the most recent version of each extension image. Use --keep to retain more versions, or --all to also clean non-extension images. Use --containers to also clean up stopped containers.`, Example: ` # Remove all but the latest version of each extension r2r cleanup # Keep 3 versions of each extension r2r cleanup --keep 3 # Also clean up stopped containers r2r cleanup --containers # Clean only extension containers r2r cleanup --containers --extensions-only # Clean containers older than 24 hours r2r cleanup --containers --older-than 24h # Show what would be removed without actually removing r2r cleanup --dry-run # Clean all Docker images (not just extensions) r2r cleanup --all`, Run: func(cmd *cobra.Command, args []string) { conf.InitConfig() if cleanupContainers { cleanupDockerContainers() logging.Info("") } if cleanupAll { cleanAllDockerImages() } else { cleanExtensionImages() } }, }
var InitCmd = &cobra.Command{ Use: "init", Short: "Initialize a new r2r-cli configuration file", Long: `Creates a minimal .r2r/r2r-cli.yml configuration file in the repository.`, Run: func(cmd *cobra.Command, args []string) { createConfigFile(cmd) }, }
var InstallCmd = &cobra.Command{ Use: "install [extension-name]", Short: "Install configured extensions or add and install new ones", Long: `Install extensions by pulling their Docker images. When no extension name is provided, installs all configured extensions. When an extension name is provided, adds it to the configuration with the latest SHA tag and installs it. Examples: # Install all configured extensions r2r install # Add and install a specific extension r2r install pwsh r2r install python r2r install go # Install with local development images r2r install pwsh --load-local`, Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 { extensionName := args[0] if err := addExtensionToConfig(extensionName); err != nil { logging.Errorf("Failed to add extension to config: %v", err) os.Exit(1) } logging.Infof("✅ Added %s to configuration", extensionName) } else { repoRoot, err := conf.FindRepositoryRoot() if err != nil { logging.Errorf("Failed to find repository root: %v", err) os.Exit(1) } configPaths := []string{ filepath.Join(repoRoot, ".r2r", "r2r-cli.yml"), filepath.Join(repoRoot, ".r2r", "r2r-cli.yaml"), } configFound := false for _, cp := range configPaths { if _, err := os.Stat(cp); err == nil { configFound = true break } } if !configFound { configPath := filepath.Join(repoRoot, ".r2r", "r2r-cli.yml") logging.Error("❌ No configuration file found.") logging.Infof("To install all configured extensions, you need a configuration file at: %s", configPath) logging.Info("\nTo get started:") logging.Info(" • Run 'r2r init' to create a configuration file") logging.Info(" • Or install a specific extension: 'r2r install <extension-name>'") logging.Info("\nExamples:") logging.Info(" r2r install pwsh") logging.Info(" r2r install python") os.Exit(1) } } conf.InitConfig() loadLocal, _ := cmd.Flags().GetBool("load-local") var originalLoadLocal bool if loadLocal { originalLoadLocal = conf.Global.LoadLocal conf.Global.LoadLocal = true logging.Debugf("Temporarily overriding load_local setting from --load-local flag: load_local=%v", true) } defer func() { if loadLocal { conf.Global.LoadLocal = originalLoadLocal logging.Debugf("Restored original load_local setting: load_local=%v", originalLoadLocal) } }() installer, err := extensions.NewInstaller() if err != nil { logging.Errorf("Failed to create extension installer: %v", err) os.Exit(1) } defer installer.Close() // Determine which extensions to install var extsToInstall []conf.Extension if len(args) > 0 { extensionName := args[0] found := false for _, ext := range conf.Global.Extensions { if ext.Name == extensionName { extsToInstall = append(extsToInstall, ext) found = true break } } if !found { logging.Errorf("❌ Extension %s not found in configuration", extensionName) os.Exit(1) } } else { extsToInstall = conf.Global.Extensions if len(extsToInstall) == 0 { logging.Error("❌ No extensions configured. Add an extension with:") logging.Info(" r2r install <extension-name>") logging.Info("\nExamples:") logging.Info(" r2r install pwsh") logging.Info(" r2r install python") os.Exit(1) } } logging.Infof("📦 Installing %d extension(s)...", len(extsToInstall)) successCount := 0 for _, ext := range extsToInstall { logging.Infof("\n🔧 Installing %s...", ext.Name) pulled, err := installer.EnsureExtensionImage(ext.Name) if err != nil { logging.Errorf("Failed to install extension: extension=%s error=%v", ext.Name, err) logging.Errorf("❌ Failed to install %s: %v", ext.Name, err) } else { if pulled { logging.Infof("✅ %s installed (new image pulled)", ext.Name) } else { logging.Infof("✅ %s already up to date", ext.Name) } successCount++ } } if successCount == len(extsToInstall) { logging.Info("\n✅ All extensions installed successfully") } else { logging.Warnf("\n⚠️ %d of %d extensions installed successfully", successCount, len(extsToInstall)) os.Exit(1) } }, }
var InteractiveCmd = &cobra.Command{ Use: "interactive <extension>", Short: "Start an extension container in interactive mode", Long: `Start an extension container in interactive mode with shell access.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { conf.InitConfig() host, err := docker.NewContainerHost() if err != nil { cmd.PrintErrln(err) os.Exit(1) } defer host.Close() if err := host.ValidateExtensions(); err != nil { cmd.PrintErrln(err) os.Exit(1) } cmd.Println("Root directory found:", host.GetRootDir()) extensionName := args[0] ext, err := host.FindExtension(extensionName) if err != nil { cmd.PrintErrln(err) os.Exit(1) } cmd.Println("Loading extension image:", ext.Image) if err := host.EnsureImageExists(ext.Image, ext.ImagePullPolicy, ext.LoadLocal); err != nil { cmd.PrintErrf("Error ensuring image exists: %v\n", err) os.Exit(1) } imageInspect, err := host.InspectImage(ext.Image) if err != nil { cmd.PrintErrln(err) os.Exit(1) } // Get extension metadata for volume mounts and env vars var volumeRequests []cache.VolumeRequest extMeta, err := host.GetExtensionMetadata(ext) if err == nil && extMeta != nil { if len(extMeta.Volumes) > 0 { volumeRequests = extMeta.Volumes } docker.MergeMetadataEnv(ext, extMeta) } containerConfig := host.CreateContainerConfig(ext, docker.ModeInteractive, nil, imageInspect) hostConfig := host.CreateHostConfig(ext, volumeRequests) containerID, err := host.CreateContainer(containerConfig, hostConfig) if err != nil { cmd.PrintErrln(err) os.Exit(1) } if err := host.StartContainer(containerID); err != nil { cmd.PrintErrln(err) os.Exit(1) } cmd.Printf("Starting interactive session for extension '%s'...\n", extensionName) cmd.Println("Type 'exit' to quit the interactive session.") signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-signalChan logging.Debugf("Received interrupt signal, stopping container gracefully: signal=%s container_id=%s", sig.String(), containerID) stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := host.StopContainerWithContext(stopCtx, containerID); err != nil { logging.Warnf("Failed to stop container gracefully: %v, forcing termination", err) host.StopContainer(containerID) } else { logging.Debug("Container stopped gracefully") } os.Exit(0) }() if len(imageInspect.Config.Entrypoint) > 0 { execCmd := exec.Command("docker", "attach", containerID) execCmd.Stdin = os.Stdin execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr if err := execCmd.Run(); err != nil { cmd.PrintErrln("Error attaching to container:", err) os.Exit(1) } } else { execCmd := exec.Command("docker", "exec", "-it", containerID, "/bin/sh") execCmd.Stdin = os.Stdin execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr if err := execCmd.Run(); err != nil { cmd.PrintErrln("Error running interactive session:", err) os.Exit(1) } } cmd.Println("Interactive session ended.") }, }
var MetadataCmd = &cobra.Command{ Use: "metadata <extension>", Short: "Retrieve metadata from an extension", Long: `Retrieve metadata from an extension by executing its extension-meta command.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { conf.InitConfig() host, err := docker.NewContainerHost() if err != nil { cmd.PrintErrf("Error creating container host: %v\n", err) os.Exit(1) } defer host.Close() if err := host.ValidateExtensions(); err != nil { cmd.PrintErrf("Error: %v\n", err) os.Exit(1) } extensionName := args[0] ext, err := host.FindExtension(extensionName) if err != nil { cmd.PrintErrf("Error: %v\n", err) os.Exit(1) } cmd.PrintErrln("Retrieving metadata from extension:", ext.Name) cmd.PrintErrln("Image:", ext.Image) if err := host.EnsureImageExists(ext.Image, ext.ImagePullPolicy, ext.LoadLocal); err != nil { cmd.PrintErrf("Error ensuring image exists: %v\n", err) os.Exit(1) } output, err := host.ExecuteMetadataCommand(ext) if err != nil { cmd.PrintErrf("Error retrieving metadata: %v\n", err) os.Exit(1) } fmt.Fprint(cmd.OutOrStdout(), output) }, }
var RootCmd = &cobra.Command{ Use: "r2r", Short: "Ready to Release - Enterprise-grade automation framework", Long: `r2r CLI standardizes and containerizes development workflows through a portable, scalable automation framework.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { parsedCmd, err := GetParsedCommand() if err != nil { return fmt.Errorf("command parsing failed: %w", err) } if logging.IsDebugEnabled() { logging.Debugf("Parsed command: subcommand=%s, extension=%s, viper_args=%v, container_args=%v", parsedCmd.Subcommand, parsedCmd.ExtensionName, parsedCmd.ViperArgs, parsedCmd.ContainerArgs) } debug, err := cmd.Flags().GetBool("r2r-debug") if err != nil { return fmt.Errorf("failed to get r2r-debug flag: %w", err) } quiet, err := cmd.Flags().GetBool("r2r-quiet") if err != nil { return fmt.Errorf("failed to get r2r-quiet flag: %w", err) } if debug && quiet { return fmt.Errorf("cannot use both debug and quiet flags") } logLevel := "info" if envLogLevel := os.Getenv("R2R_LOG_LEVEL"); envLogLevel != "" { logLevel = envLogLevel } if debug { logLevel = "debug" } else if quiet { logLevel = "error" } if err := logging.SetLevel(logLevel); err != nil { return fmt.Errorf("failed to set log level: %w", err) } if os.Getenv("R2R_FIXED_REDIRECT") == "true" { logging.Warnf("Fixed bash redirect pollution in arguments (removed spurious '2' from '2>&1'): original=%s filtered=%s", os.Getenv("R2R_ORIGINAL_ARGS"), os.Getenv("R2R_FILTERED_ARGS")) os.Unsetenv("R2R_FIXED_REDIRECT") os.Unsetenv("R2R_ORIGINAL_ARGS") os.Unsetenv("R2R_FILTERED_ARGS") } logging.Debugf("Executing command: cmd=%s args=%v version=%s os=%s arch=%s", cmd.Name(), args, version.Version, runtime.GOOS, runtime.GOARCH) return nil }, Run: func(cmd *cobra.Command, args []string) { if err := cmd.Help(); err != nil { logging.Errorf("Failed to show help: %v", err) } }, }
RootCmd is the base command for the r2r CLI when called without any subcommands
var RunCmd = &cobra.Command{ Use: "run <extension> [args...]", Short: "Run an extension from the config", Long: `Run an extension using its configured Docker image.`, DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { cmd.Help() return } if len(args) == 0 { cmd.Help() return } parsedCmd, _ := GetParsedCommand() extensionName := args[0] containerArgs := args[1:] if parsedCmd != nil && parsedCmd.Subcommand == "run" { if parsedCmd.ExtensionName != "" { extensionName = parsedCmd.ExtensionName } if len(parsedCmd.ContainerArgs) > 0 || parsedCmd.ArgumentBoundary > 0 { containerArgs = parsedCmd.ContainerArgs } } logging.Debugf("Running extension: extension=%s args=%v parsed_boundary=%d", extensionName, containerArgs, parsedCmd.ArgumentBoundary) if len(containerArgs) == 0 { logging.Debug("No arguments provided, switching to interactive mode") InteractiveCmd.Run(cmd, []string{extensionName}) return } conf.InitConfig() logging.Debug("Creating extension installer") installer, err := extensions.NewInstaller() if err != nil { logging.Errorf("Failed to create extension installer: %v", err) os.Exit(1) } defer installer.Close() host := installer.GetContainerHost() logging.Debug("Validating extensions") if err := host.ValidateExtensions(); err != nil { logging.Errorf("Extension validation failed: %v", err) os.Exit(1) } logging.Debugf("Root directory found: root_dir=%s", host.GetRootDir()) logging.Debugf("Available extensions in config: extension_count=%d", len(conf.Global.Extensions)) for _, ext := range conf.Global.Extensions { logging.Debugf("Extension found in config: name=%s image=%s", ext.Name, ext.Image) } logging.Debugf("Finding extension: extension=%v", extensionName) ext, err := host.FindExtension(extensionName) if err != nil { logging.Errorf("Extension '%s' not found", extensionName) os.Stdout.Sync() os.Stderr.Sync() os.Exit(1) } logging.Debugf("Loading extension image: image=%s", ext.Image) beforeSnapshot, err := host.GetContainerSnapshot() if err != nil { logging.Debugf("Failed to take container snapshot before run: error=%v", err) beforeSnapshot = make(map[string]string) } logging.Debugf("Ensuring image exists: image=%s pull_policy=%s", ext.Image, ext.ImagePullPolicy) if _, err := installer.EnsureExtensionImage(extensionName); err != nil { logging.Errorf("Error ensuring image exists: %v", err) os.Exit(1) } logging.Debugf("Inspecting image: image=%v", ext.Image) imageInspect, err := host.InspectImage(ext.Image) if err != nil { logging.Errorf("Failed to inspect image '%s': %v", ext.Image, err) os.Exit(1) } // Get extension metadata for volume mounts and env vars var volumeRequests []cache.VolumeRequest extMeta, err := host.GetExtensionMetadata(ext) if err != nil { logging.Debugf("Failed to get extension metadata, continuing without metadata") } else if extMeta != nil { if len(extMeta.Volumes) > 0 { volumeRequests = extMeta.Volumes logging.Debugf("Loaded volume requests from extension metadata: extension=%s volumes=%d", ext.Name, len(volumeRequests)) } docker.MergeMetadataEnv(ext, extMeta) } containerConfig := host.CreateContainerConfig(ext, docker.ModeRun, containerArgs, imageInspect) hostConfig := host.CreateHostConfig(ext, volumeRequests) logging.Debug("Creating container") containerID, err := host.CreateContainer(containerConfig, hostConfig) if err != nil { logging.Errorf("Failed to create container: %v", err) os.Exit(1) } logging.Debugf("Container created: container_id=%v", containerID) logging.Debugf("Attaching to container: container_id=%v", containerID) attachResp, err := host.AttachToContainer(containerID) if err != nil { logging.Errorf("Failed to attach to container %s: %v", containerID, err) os.Exit(1) } defer attachResp.Close() logging.Debugf("Setting up container wait: container_id=%v", containerID) statusCh, errCh := host.WaitForContainer(containerID) logging.Debugf("Starting container: container_id=%v", containerID) if err := host.StartContainer(containerID); err != nil { logging.Errorf("Failed to start container %s: %v", containerID, err) os.Exit(1) } signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) shuttingDown := false go func() { sig := <-signalChan shuttingDown = true logging.Debugf("Received interrupt signal, stopping container gracefully: signal=%s container_id=%s", sig.String(), containerID) go func() { if docker.IsRunningInContainer() { logging.Debug("Detected Docker-in-Docker, cleaning up child containers") if err := host.CleanupChildContainers(); err != nil { logging.Warnf("Failed to clean up some child containers: error=%v", err) } } stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := host.StopContainerWithContext(stopCtx, containerID); err != nil { logging.Warnf("Failed to stop container gracefully, forcing termination: container_id=%s error=%v", containerID, err) if err := host.StopContainer(containerID); err != nil { logging.Errorf("Failed to force stop container: %v", err) } } else { logging.Debugf("Container stopped gracefully: container_id=%s", containerID) } }() time.Sleep(100 * time.Millisecond) os.Exit(130) }() done := make(chan error, 1) if containerConfig.Tty { go func() { ansiFilter := docker.NewAnsiFilter(os.Stdout) _, err := io.Copy(ansiFilter, attachResp.Reader) done <- err }() } else { go func() { _, err := stdcopy.StdCopy(os.Stdout, os.Stderr, attachResp.Reader) done <- err }() } if containerConfig.OpenStdin { go func() { defer func() { if conn, ok := attachResp.Conn.(interface { CloseWrite() error }); ok { conn.CloseWrite() } }() _, err := io.Copy(attachResp.Conn, os.Stdin) if err != nil && err != io.EOF { logging.Debugf("stdin copy error: %v", err) } }() } logging.Debugf("Waiting for container to finish: container_id=%v", containerID) // Wait for container completion var containerExitCode int64 select { case status := <-statusCh: logging.Debugf("Container finished: container_id=%s status_code=%d", containerID, status.StatusCode) containerExitCode = status.StatusCode case err := <-errCh: if err != nil { errStr := err.Error() if !strings.Contains(errStr, "No such container") && errStr != "" { logging.Errorf("Error waiting for container: container_id=%s error=%s", containerID, errStr) os.Exit(1) } } } if shuttingDown { os.Exit(0) } ioErr := <-done if ioErr != nil && ioErr != io.EOF { logging.Debugf("I/O error: error=%v", ioErr) } logging.Debug("I/O copy completed") afterSnapshot, err := host.GetContainerSnapshot() if err != nil { logging.Debugf("Failed to take container snapshot after run: error=%v", err) } else { // Get expected host images from extension metadata (for serve commands, etc.) var expectedHostImages []string if extMeta != nil { expectedHostImages = extMeta.ExpectedHostImages } host.WarnAboutNewContainers(beforeSnapshot, afterSnapshot, ext.Image, ext.AutoRemoveChildren, expectedHostImages) } if docker.IsRunningInContainer() { logging.Debug("Cleaning up any remaining child containers before exit") if err := host.CleanupChildContainers(); err != nil { logging.Warnf("Failed to clean up some child containers: error=%v", err) } } if !shuttingDown && containerExitCode != 0 { os.Exit(int(containerExitCode)) } }, }
var VerifyCmd = &cobra.Command{ Use: "verify", Short: "Verify system prerequisites", Long: `Verifies that GitHub authentication and Docker service are working properly.`, Run: func(cmd *cobra.Command, args []string) { verifySystem(cmd) }, }
var Version string
Version is set at build time via ldflags
Functions ¶
func CreateExtensionAliases ¶
func CreateExtensionAliases()
CreateExtensionAliases creates direct command aliases for configured extensions This allows users to run "r2r pwsh" instead of "r2r run pwsh"
func GetContainerArgs ¶
func GetContainerArgs() []string
GetContainerArgs returns the container arguments from the parsed command This is used by the run command to get arguments that should be passed to the container
func GetExtensionName ¶
func GetExtensionName() string
GetExtensionName returns the extension name from the parsed command
func GetParsedCommand ¶
func GetParsedCommand() (*commandparser.ParsedCommand, error)
GetParsedCommand returns the parsed command structure This is populated during the root command's PersistentPreRun
func GetViperArgs ¶
func GetViperArgs() []string
GetViperArgs returns the arguments that should be processed by Viper/Cobra
func InitializeExtensionAliases ¶
func InitializeExtensionAliases()
InitializeExtensionAliases should be called after config is loaded but before command execution
func IsRunCommand ¶
func IsRunCommand() bool
IsRunCommand checks if the current command is a run command