cmd

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2023 License: MIT Imports: 35 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	AddressFlag = &cli.StringFlag{
		Name:  "address",
		Value: api.DefaultAddress,
		Usage: "Specify the url for the Nullstone API.",
	}
	ApiKeyFlag = &cli.StringFlag{
		Name:  "api-key",
		Value: "",
		Usage: "Configure your personal API key that will be used to authenticate with the Nullstone API. You can generate an API key from your profile page.",
	}
)
View Source
var AppFlag = &cli.StringFlag{
	Name:    "app",
	Usage:   "Name of the app to use for this operation",
	EnvVars: []string{"NULLSTONE_APP"},
}
View Source
var AppSourceFlag = &cli.StringFlag{
	Name: "source",
	Usage: `The source artifact to push that contains your application's build.
		For a container, specify the name of the docker image to push. This follows the same syntax as 'docker push NAME[:TAG]'.
		For a serverless zip application, specify the .zip archive to push.
		For a static site, specify the directory to push.`,
	Required: true,
}
View Source
var AppVersionFlag = &cli.StringFlag{
	Name: "version",
	Usage: `Provide a label for your deployment.
		If not provided, it will default to the commit sha of the repo for the current directory.`,
}
View Source
var Apply = func() *cli.Command {
	return &cli.Command{
		Name: "apply",
		Description: "Runs a Terraform apply on the given block and environment. This is useful for making ad-hoc changes to your infrastructure.\n" +
			"This plan will be executed by the Nullstone system. In order to run a plan locally, check out the `nullstone workspaces select` command.\n" +
			"Be sure to run `nullstone plan` first to see what changes will be made.",
		Usage:     "Runs an apply with optional auto-approval",
		UsageText: "nullstone apply [--stack=<stack-name>] --block=<block-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			BlockFlag,
			EnvFlag,
			&cli.BoolFlag{
				Name:    "wait",
				Aliases: []string{"w"},
				Usage:   "Wait for the apply to complete and stream the Terraform logs to the console.",
			},
			&cli.BoolFlag{
				Name:  "auto-approve",
				Usage: "Skip any approvals and apply the changes immediately. This requires proper permissions in the stack.",
			},
			&cli.StringSliceFlag{
				Name:  "var",
				Usage: "Set variables values for the apply. This can be used to override variables defined in the module.",
			},
			&cli.StringFlag{
				Name:  "module-version",
				Usage: "The version of the module to apply.",
			},
		},
		Action: func(c *cli.Context) error {
			varFlags := c.StringSlice("var")
			moduleVersion := c.String("module-version")
			var autoApprove *bool
			if c.IsSet("auto-approve") {
				val := c.Bool("auto-approve")
				autoApprove = &val
			}

			return BlockWorkspaceAction(c, func(ctx context.Context, cfg api.Config, stack types.Stack, block types.Block, env types.Environment, workspace types.Workspace) error {
				if moduleVersion != "" {
					module := types.WorkspaceModuleInput{
						Module:        block.ModuleSource,
						ModuleVersion: moduleVersion,
					}
					err := runs.SetModuleVersion(cfg, workspace, module)
					if err != nil {
						return err
					}
				}

				err := runs.SetConfigVars(cfg, workspace, varFlags)
				if err != nil {
					return err
				}

				newRun, err := runs.Create(cfg, workspace, autoApprove, false)
				if err != nil {
					return fmt.Errorf("error creating run: %w", err)
				} else if newRun == nil {
					return fmt.Errorf("unable to create run")
				}
				fmt.Fprintf(os.Stdout, "created apply run %q\n", newRun.Uid)
				fmt.Fprintln(os.Stdout, runs.GetBrowserUrl(cfg, workspace, *newRun))

				if c.IsSet("wait") {
					return runs.StreamLogs(ctx, cfg, workspace, newRun)
				}
				return nil
			})
		},
	}
}
View Source
var Apps = &cli.Command{
	Name:      "apps",
	Usage:     "View and modify applications",
	UsageText: "nullstone apps [subcommand]",
	Subcommands: []*cli.Command{
		AppsList,
	},
}
View Source
var AppsList = &cli.Command{
	Name:        "list",
	Description: "Shows a list of the applications that you have access to. Set the `--detail` flag to show more details about each application.",
	Usage:       "List applications",
	UsageText:   "nullstone apps list",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
			Usage:   "Use this flag to show the details for each application",
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			client := api.Client{Config: cfg}
			allApps, err := client.Apps().List()
			if err != nil {
				return fmt.Errorf("error listing applications: %w", err)
			}

			if c.IsSet("detail") {
				appDetails := make([]string, len(allApps)+1)
				appDetails[0] = "ID|Name|Reference|Category|Type|Module|Stack|Framework"
				for i, app := range allApps {
					var appCategory types.CategoryName
					var appType string
					if appModule, err := find.Module(cfg, app.ModuleSource); err == nil {
						appCategory = appModule.Category
						appType = appModule.Type
					}
					stack, err := client.Stacks().Get(app.StackId)
					if err != nil {
						return fmt.Errorf("error looking for stack %q: %w", app.StackId, err)
					}
					appDetails[i+1] = fmt.Sprintf("%d|%s|%s|%s|%s|%s|%s|%s", app.Id, app.Name, app.Reference, appCategory, appType, app.ModuleSource, stack.Name, app.Framework)
				}
				fmt.Println(columnize.Format(appDetails, columnize.DefaultConfig()))
			} else {
				for _, app := range allApps {
					fmt.Println(app.Name)
				}
			}

			return nil
		})
	},
}
View Source
var BlockFlag = &cli.StringFlag{
	Name:     "block",
	Usage:    "Name of the block to use for this operation",
	EnvVars:  []string{"NULLSTONE_BLOCK", "NULLSTONE_APP"},
	Required: true,
}
View Source
var Blocks = &cli.Command{
	Name:      "blocks",
	Usage:     "View and modify blocks",
	UsageText: "nullstone blocks [subcommand]",
	Subcommands: []*cli.Command{
		BlocksList,
		BlocksNew,
	},
}
View Source
var BlocksList = &cli.Command{
	Name:        "list",
	Description: "Shows a list of the blocks for the given stack. Set the `--detail` flag to show more details about each block.",
	Usage:       "List blocks",
	UsageText:   "nullstone blocks list --stack=<stack>",
	Flags: []cli.Flag{
		StackRequiredFlag,
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
			Usage:   "Use this flag to show more details about each block",
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			client := api.Client{Config: cfg}

			stackName := c.String(StackRequiredFlag.Name)
			stack, err := client.StacksByName().Get(stackName)
			if err != nil {
				return fmt.Errorf("error looking for stack %q: %w", stackName, err)
			} else if stack == nil {
				return fmt.Errorf("stack %q does not exist in organization %q", stackName, cfg.OrgName)
			}

			allBlocks, err := client.Blocks().List(stack.Id)
			if err != nil {
				return fmt.Errorf("error listing blocks: %w", err)
			}

			if c.IsSet("detail") {
				appDetails := make([]string, len(allBlocks)+1)
				appDetails[0] = "ID|Type|Name|Reference|Category|Module Type|Module|Stack"
				for i, block := range allBlocks {
					var blockCategory types.CategoryName
					var blockType string
					if blockModule, err := find.Module(cfg, block.ModuleSource); err == nil {
						blockCategory = blockModule.Category
						blockType = blockModule.Type
					}
					appDetails[i+1] = fmt.Sprintf("%d|%s|%s|%s|%s|%s|%s|%s", block.Id, block.Type, block.Name, block.Reference, blockCategory, blockType, block.ModuleSource, stackName)
				}
				fmt.Println(columnize.Format(appDetails, columnize.DefaultConfig()))
			} else {
				for _, block := range allBlocks {
					fmt.Println(block.Name)
				}
			}

			return nil
		})
	},
}
View Source
var BlocksNew = &cli.Command{
	Name:        "new",
	Description: "Creates a new block with the given name and module. If the module has any connections, you can specify them using the `--connection` parameter.",
	Usage:       "Create block",
	UsageText:   "nullstone blocks new --name=<name> --stack=<stack> --module=<module> [--connection=<connection>...]",
	Flags: []cli.Flag{
		StackRequiredFlag,
		&cli.StringFlag{
			Name:     "name",
			Required: true,
			Usage:    "Provide a name for this new block",
		},
		&cli.StringFlag{
			Name:     "module",
			Usage:    `Specify the unique name of the module to use for this block. Example: nullstone/aws-network`,
			Required: true,
		},
		&cli.StringSliceFlag{
			Name:  "connection",
			Usage: "Specify any connections that this block will have to other blocks. Use the connection name as the key, and the connected block name as the value. Example: --connection network=network0",
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			client := api.Client{Config: cfg}

			stackName := c.String(StackRequiredFlag.Name)
			stack, err := client.StacksByName().Get(stackName)
			if err != nil {
				return fmt.Errorf("error looking for stack %q: %w", stackName, err)
			} else if stack == nil {
				return fmt.Errorf("stack %q does not exist in organization %q", stackName, cfg.OrgName)
			}

			name := c.String("name")
			moduleSource := c.String("module")
			if !strings.Contains(moduleSource, "/") {

				moduleSource = fmt.Sprintf("%s/%s", cfg.OrgName, moduleSource)
			}
			connectionSlice := c.StringSlice("connection")

			module, err := find.Module(cfg, moduleSource)
			if err != nil {
				return err
			}

			connections, err := mapConnectionsToTargets(cfg, stack, connectionSlice)
			if err != nil {
				return err
			}
			if err := validateConnections(module.LatestVersion, connections); err != nil {
				return err
			}

			block := &types.Block{
				OrgName:             cfg.OrgName,
				StackId:             stack.Id,
				Type:                blockTypeFromModuleCategory(module.Category),
				Name:                name,
				ModuleSource:        moduleSource,
				ModuleSourceVersion: "latest",
				Connections:         connections,
			}
			if strings.HasPrefix(string(module.Category), "app") {
				app := &types.Application{
					Block:     *block,
					Repo:      "",
					Framework: "other",
				}
				if newApp, err := client.Apps().Create(stack.Id, app); err != nil {
					return err
				} else if newApp != nil {
					fmt.Printf("created %s app\n", newApp.Name)
				} else {
					fmt.Println("unable to create app")
				}
			} else {
				if newBlock, err := client.Blocks().Create(stack.Id, block); err != nil {
					return err
				} else if newBlock != nil {
					fmt.Printf("created %q block\n", newBlock.Name)
				} else {
					fmt.Println("unable to create block")
				}
			}
			return nil
		})
	},
}
View Source
var Configure = &cli.Command{
	Name:        "configure",
	Description: "Establishes a profile and configures authentication for the CLI to use.",
	Usage:       "Configure the nullstone CLI",
	UsageText:   "nullstone configure --api-key=<api-key>",
	Flags: []cli.Flag{
		AddressFlag,
		ApiKeyFlag,
	},
	Action: func(c *cli.Context) error {
		apiKey := c.String(ApiKeyFlag.Name)
		if apiKey == "" {
			fmt.Print("Enter API Key: ")
			rawApiKey, err := terminal.ReadPassword(int(syscall.Stdin))
			if err != nil {
				return fmt.Errorf("error reading password: %w", err)
			}
			fmt.Println()
			apiKey = string(rawApiKey)
		}

		profile := config.Profile{
			Name:    GetProfile(c),
			Address: c.String(AddressFlag.Name),
			ApiKey:  apiKey,
		}
		if err := profile.Save(); err != nil {
			return fmt.Errorf("error configuring profile: %w", err)
		}
		fmt.Fprintln(os.Stderr, "nullstone configured successfully!")
		return nil
	},
}
View Source
var ContainerFlag = &cli.StringFlag{
	Name: "container",
	Usage: `Select a specific container within a task or pod.
        If using sidecars, this allows you to connect to other containers besides the primary application container.`,
}
View Source
var Deploy = func(providers app.Providers) *cli.Command {
	return &cli.Command{
		Name:        "deploy",
		Description: "Deploy a new version of your code for this application. This command works in tandem with the `nullstone push` command. This command deploys the artifacts that were uploaded during the `push` command.",
		Usage:       "Deploy application",
		UsageText:   "nullstone deploy [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			OldEnvFlag,
			AppVersionFlag,
			&cli.BoolFlag{
				Name:    "wait",
				Aliases: []string{"w"},
				Usage:   "Wait for the deploy to complete and stream the logs to the console.",
			},
		},
		Action: func(c *cli.Context) error {
			return AppWorkspaceAction(c, func(ctx context.Context, cfg api.Config, appDetails app.Details) error {
				version, wait := DetectAppVersion(c), c.IsSet("wait")
				if version == "" {
					return fmt.Errorf("no version specified, version is required to create a deploy")
				}

				fmt.Fprintln(os.Stderr, "Creating deploy...")
				deploy, err := CreateDeploy(cfg, appDetails, version)
				if err != nil {
					return err
				}

				fmt.Fprintln(os.Stderr)
				return streamDeployLogs(ctx, cfg, *deploy, wait)
			})
		},
	}
}
View Source
var EnvFlag = &cli.StringFlag{
	Name:     "env",
	Usage:    `Name of the environment to use for this operation`,
	EnvVars:  []string{"NULLSTONE_ENV"},
	Required: true,
}
View Source
var EnvOptionalFlag = &cli.StringFlag{
	Name:     "env",
	Usage:    `Name of the environment to use for this operation`,
	EnvVars:  []string{"NULLSTONE_ENV"},
	Required: false,
}
View Source
var Envs = &cli.Command{
	Name:      "envs",
	Usage:     "View and modify environments",
	UsageText: "nullstone envs [subcommand]",
	Subcommands: []*cli.Command{
		EnvsList,
		EnvsNew,
		EnvsDelete,
		EnvsUp,
		EnvsDown,
	},
}
View Source
var EnvsDelete = &cli.Command{
	Name:        "delete",
	Description: "Deletes the given environment. Before issuing this command, make sure you have destroyed all infrastructure in the environment. If you are deleting a preview environment, you can use the `--force` flag to skip the confirmation prompt.",
	Usage:       "Create new environment",
	UsageText:   "nullstone envs delete --stack=<stack> --env=<env>	[--force]",
	Flags: []cli.Flag{
		StackRequiredFlag,
		EnvFlag,
		&cli.BoolFlag{
			Name:  "force",
			Usage: "Use this flag to skip the confirmation prompt when deleting an environment.",
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			client := api.Client{Config: cfg}
			stackName := c.String("stack")
			envName := c.String("env")
			force := c.IsSet("force")

			stack, err := client.StacksByName().Get(stackName)
			if err != nil {
				return fmt.Errorf("error looking for stack %q: %w", stackName, err)
			} else if stack == nil {
				return fmt.Errorf("stack %q does not exist", stackName)
			}

			env, err := find.Env(cfg, stack.Id, envName)
			if err != nil {
				return fmt.Errorf("error looking for environment in stack %q - %q: %w", stack.Name, envName, err)
			} else if env == nil {
				return fmt.Errorf("environment %q does not exist in stack %q", envName, stack.Name)
			}

			if !force {
				fmt.Printf("You are about to delete an environment. Make sure you have destroyed all infrastructure in the environment before continuing.\n")
				confirm := []*survey.Question{
					{
						Name:     "Confirm",
						Validate: survey.Required,
						Prompt: &survey.Input{
							Message: "To confirm all infrastructure has been destroyed and you wish to continue, type 'delete':",
						},
					},
				}
				var confirmResponse string
				err := survey.Ask(confirm, &confirmResponse)
				if err != nil {
					return err
				}
				if confirmResponse != "delete" {
					fmt.Println("Deletion of the environment has been cancelled")
					return nil
				}
			}

			_, err = client.Environments().Destroy(stack.Id, env.Id)
			if err != nil {
				return fmt.Errorf("error deleting environment: %w", err)
			}

			fmt.Printf("Environment %s has been deleted\n", env.Name)
			return nil
		})
	},
}
View Source
var EnvsDown = &cli.Command{
	Name:        "down",
	Description: "Destroys all the apps in an environment and all their dependent infrastructure. This command is useful for tearing down preview environments once you are finished with them.",
	Usage:       "Destroy an entire environment",
	UsageText:   "nullstone envs down --stack=<stack> --env=<env>",
	Flags: []cli.Flag{
		StackRequiredFlag,
		EnvFlag,
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			if err := createEnvRun(c, cfg, true); err != nil {
				return fmt.Errorf("error when trying to destroy environment: %w", err)
			}
			return nil
		})
	},
}
View Source
var EnvsList = &cli.Command{
	Name:        "list",
	Description: "Shows a list of the environments for the given stack. Set the `--detail` flag to show more details about each environment.",
	Usage:       "List environments",
	UsageText:   "nullstone envs list --stack=<stack-name>",
	Flags: []cli.Flag{
		StackRequiredFlag,
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
			Usage:   "Use this flag to show more details about each environment",
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			stackName := c.String(StackRequiredFlag.Name)
			stack, err := find.Stack(cfg, stackName)
			if err != nil {
				return fmt.Errorf("error retrieving stack: %w", err)
			} else if stack == nil {
				return fmt.Errorf("stack %s does not exist", stackName)
			}

			client := api.Client{Config: cfg}
			envs, err := client.Environments().List(stack.Id)
			if err != nil {
				return fmt.Errorf("error listing environments: %w", err)
			}
			sort.SliceStable(envs, func(i, j int) bool {
				var first int
				if envs[i].PipelineOrder == nil {
					first = math.MaxInt
				} else {
					first = *envs[i].PipelineOrder
				}
				var second int
				if envs[j].PipelineOrder == nil {
					second = math.MaxInt
				} else {
					second = *envs[j].PipelineOrder
				}
				return first < second
			})

			if c.IsSet("detail") {
				envDetails := make([]string, len(envs)+1)
				envDetails[0] = "ID|Name|Type"
				for i, env := range envs {
					envDetails[i+1] = fmt.Sprintf("%d|%s|%s", env.Id, env.Name, strings.TrimSuffix(string(env.Type), "Env"))
				}
				fmt.Println(columnize.Format(envDetails, columnize.DefaultConfig()))
			} else {
				for _, env := range envs {
					fmt.Println(env.Name)
				}
			}

			return nil
		})
	},
}
View Source
var EnvsNew = &cli.Command{
	Name:        "new",
	Description: "Creates a new environment in the given stack. If the `--preview` parameter is set, a preview environment will be created and the `--provider` parameter will not be used. Otherwise, a standard environment will be created as the last environment in the pipeline. Specify the provider, region, and zone to determine where infrastructure will be provisioned for this environment.",
	Usage:       "Create new environment",
	UsageText:   "nullstone envs new --name=<name> --stack=<stack> [--provider=<provider>] [--preview]",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:     "name",
			Usage:    "Provide a name for this new environment",
			Required: true,
		},
		StackRequiredFlag,
		&cli.BoolFlag{
			Name:  "preview",
			Usage: "Use this flag to create a preview environment. If not set, a standard environment will be created.",
		},
		&cli.StringFlag{
			Name:  "provider",
			Usage: "Select the name of the provider to use for this environment. When creating a preview environment, this parameter will not be used.",
		},
		&cli.StringFlag{
			Name:  "region",
			Usage: fmt.Sprintf("Select which region to launch infrastructure for this environment. Defaults to %s for AWS and %s for GCP.", awsDefaultRegion, gcpDefaultRegion),
		},
		&cli.StringFlag{
			Name:  "zone",
			Usage: fmt.Sprintf("For GCP, select the zone to launch infrastructure for this environment. Defaults to %s", gcpDefaultZone),
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			client := api.Client{Config: cfg}
			name := c.String("name")
			stackName := c.String("stack")
			providerName := c.String("provider")
			region := c.String("region")
			zone := c.String("zone")
			preview := c.IsSet("preview")

			stack, err := client.StacksByName().Get(stackName)
			if err != nil {
				return fmt.Errorf("error looking for stack %q: %w", stackName, err)
			} else if stack == nil {
				return fmt.Errorf("stack %q does not exist", stackName)
			}

			if preview {
				return createPreviewEnv(client, stack.Id, name)
			} else {
				return createPipelineEnv(client, stack.Id, name, providerName, region, zone)
			}
		})
	},
}
View Source
var EnvsUp = &cli.Command{
	Name:        "up",
	Description: "Launches an entire environment including all of its apps. This command can be used to stand up an entire preview environment.",
	Usage:       "Launch an entire environment",
	UsageText:   "nullstone envs up --stack=<stack> --env=<env>",
	Flags: []cli.Flag{
		StackRequiredFlag,
		EnvFlag,
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			if err := createEnvRun(c, cfg, false); err != nil {
				return fmt.Errorf("error when trying to launch environment: %w", err)
			}
			return nil
		})
	},
}
View Source
var (
	ErrMissingOrg = errors.New("An organization has not been configured with this profile. See 'nullstone set-org -h' for more details.")
)
View Source
var Exec = func(providers admin.Providers) *cli.Command {
	return &cli.Command{
		Name:        "exec",
		Description: "Executes a command on a container or the virtual machine for the given application. Defaults command to '/bin/sh' which acts as opening a shell to the running container or virtual machine.",
		Usage:       "Execute a command on running service",
		UsageText:   "nullstone exec [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options] [command]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			EnvFlag,
			TaskFlag,
			PodFlag,
			ContainerFlag,
		},
		Action: func(c *cli.Context) error {
			cmd := []string{"/bin/sh"}
			if c.Args().Present() {
				cmd = c.Args().Slice()
			}

			return AppWorkspaceAction(c, func(ctx context.Context, cfg api.Config, appDetails app.Details) error {
				remoter, err := providers.FindRemoter(logging.StandardOsWriters{}, cfg, appDetails)
				if err != nil {
					return err
				}
				options := admin.RemoteOptions{
					Task:      c.String("task"),
					Pod:       c.String("pod"),
					Container: c.String("container"),
				}
				return remoter.Exec(ctx, options, cmd)
			})
		},
	}
}
View Source
var Launch = func(providers app.Providers) *cli.Command {
	return &cli.Command{
		Name: "launch",
		Description: "This command will first upload (push) an artifact containing the source for your application. Then it will deploy it to the given environment and tail the logs for the deployment." +
			"This command is the same as running `nullstone push` followed by `nullstone deploy -w`.",
		Usage:     "Launch application (push + deploy + wait-healthy)",
		UsageText: "nullstone launch [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			OldEnvFlag,
			AppSourceFlag,
			AppVersionFlag,
		},
		Action: func(c *cli.Context) error {
			return AppWorkspaceAction(c, func(ctx context.Context, cfg api.Config, appDetails app.Details) error {
				source, version := c.String("source"), DetectAppVersion(c)
				osWriters := logging.StandardOsWriters{}
				factory := providers.FindFactory(*appDetails.Module)
				if factory == nil {
					return fmt.Errorf("this app module is not supported")
				}

				err := push(ctx, cfg, appDetails, osWriters, factory, source, version)
				if err != nil {
					return err
				}

				fmt.Fprintln(os.Stderr, "Creating deploy...")
				deploy, err := CreateDeploy(cfg, appDetails, version)
				if err != nil {
					return err
				}

				fmt.Fprintln(os.Stderr)
				return streamDeployLogs(ctx, cfg, *deploy, true)
			})
		},
	}
}

Launch command performs push, deploy, and logs

View Source
var Logs = func(providers admin.Providers) *cli.Command {
	return &cli.Command{
		Name:        "logs",
		Description: "Streams an application's logs to the console for the given environment. Use the start-time `-s` and end-time `-e` flags to only show logs for a given time period. Use the tail flag `-t` to stream the logs in real time.",
		Usage:       "Emit application logs",
		UsageText:   "nullstone logs [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			OldEnvFlag,
			&cli.DurationFlag{
				Name:        "start-time",
				Aliases:     []string{"s"},
				DefaultText: "0s",
				Usage: `
       Emit log events that occur after the specified start-time. 
       This is a golang duration relative to the time the command is issued.
       Examples: '5s' (5 seconds ago), '1m' (1 minute ago), '24h' (24 hours ago)
      `,
			},
			&cli.DurationFlag{
				Name:    "end-time",
				Aliases: []string{"e"},
				Usage: `
       Emit log events that occur before the specified end-time. 
       This is a golang duration relative to the time the command is issued.
       Examples: '5s' (5 seconds ago), '1m' (1 minute ago), '24h' (24 hours ago)
      `,
			},
			&cli.DurationFlag{
				Name:        "interval",
				DefaultText: "1s",
				Usage: `Set --interval to a golang duration to control how often to pull new log events.
       This will do nothing unless --tail is set. The default is '1s' (1 second).
      `,
			},
			&cli.BoolFlag{
				Name:    "tail",
				Aliases: []string{"t"},
				Usage: `Set tail to watch log events and emit as they are reported.
       Use --interval to control how often to query log events.
       This is off by default. Unless this option is provided, this command will exit as soon as current log events are emitted.`,
			},
		},
		Action: func(c *cli.Context) error {
			logStreamOptions := config.LogStreamOptions{
				WatchInterval: -1 * time.Second,
			}
			if c.IsSet("start-time") {
				absoluteTime := time.Now().Add(-c.Duration("start-time"))
				logStreamOptions.StartTime = &absoluteTime
			} else {
				absoluteTime := time.Now()
				logStreamOptions.StartTime = &absoluteTime
			}
			if c.IsSet("end-time") {
				absoluteTime := time.Now().Add(-c.Duration("end-time"))
				logStreamOptions.EndTime = &absoluteTime
			}
			if c.IsSet("tail") {
				logStreamOptions.WatchInterval = time.Duration(0)
				if c.IsSet("interval") {
					logStreamOptions.WatchInterval = c.Duration("interval")
				}
			}

			return AppWorkspaceAction(c, func(ctx context.Context, cfg api.Config, appDetails app.Details) error {
				logStreamer, err := providers.FindLogStreamer(logging.StandardOsWriters{}, cfg, appDetails)
				if err != nil {
					return err
				}
				return logStreamer.Stream(ctx, logStreamOptions)
			})
		},
	}
}
View Source
var Modules = &cli.Command{
	Name:      "modules",
	Usage:     "View and modify modules",
	UsageText: "nullstone modules [subcommand]",
	Subcommands: []*cli.Command{
		ModulesGenerate,
		ModulesRegister,
		ModulesPublish,
		ModulesPackage,
	},
}
View Source
var ModulesGenerate = &cli.Command{
	Name: "generate",
	Description: "Generates a nullstone manifest file for your module in the current directory. " +
		"You will be asked a series of questions in order to collect the information needed to describe a Nullstone module. " +
		"Optionally, you can also register the module in the Nullstone registry by passing the `--register` flag.",
	Usage:     "Generate new module manifest (and optionally register)",
	UsageText: "nullstone modules generate [--register]",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:  "register",
			Usage: "Register the module in the Nullstone registry after generating the manifest file.",
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			existing, _ := modules.ManifestFromFile(moduleManifestFilename)
			survey := &moduleSurvey{}
			manifest, err := survey.Ask(cfg, existing)
			if err != nil {
				return err
			}
			if err := manifest.WriteManifestToFile(moduleManifestFilename); err != nil {
				return err
			}
			fmt.Printf("generated module manifest file to %s\n", moduleManifestFilename)

			if err := modules.Generate(manifest); err != nil {
				return err
			}
			fmt.Printf("generated base Terraform\n")

			if c.IsSet("register") {
				module, err := modules.Register(cfg, manifest)
				if err != nil {
					return err
				}
				fmt.Printf("registered %s/%s\n", module.OrgName, module.Name)
			}
			return nil
		})
	},
}
View Source
var ModulesPackage = &cli.Command{
	Name:        "package",
	Description: "Package all the module contents for a Nullstone module into a tarball but do not publish to the registry.",
	Usage:       "Package a module",
	UsageText:   "nullstone modules package",
	Flags: []cli.Flag{
		includeFlag,
	},
	Action: func(c *cli.Context) error {
		includes := c.StringSlice("include")

		manifest, err := modules.ManifestFromFile(moduleManifestFilename)
		if err != nil {
			return err
		}

		tarballFilename, err := modules.Package(manifest, "", includes)
		if err == nil {
			fmt.Printf("created module package %q\n", tarballFilename)
		}
		return err
	},
}
View Source
var ModulesPublish = &cli.Command{
	Name:        "publish",
	Description: "Publishes a new version for a module in the Nullstone registry. Provide a specific semver version using the `--version` parameter.",
	Usage:       "Package and publish new version of a module",
	UsageText:   "nullstone modules publish --version=<version>",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "version",
			Aliases: []string{"v"},
			Usage: `Specify a semver version for the module.
'next-patch': Uses a version that bumps the patch component of the latest module version.
'next-build': Uses the latest version and appends +<build> using the short Git commit SHA. (Fails if not in a Git repository)`,
			Required: true,
		},
		includeFlag,
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			version := c.String("version")
			includes := c.StringSlice("include")

			manifest, err := modules.ManifestFromFile(moduleManifestFilename)
			if err != nil {
				return err
			}

			if version == "next-patch" {
				version, err = modules.NextPatch(cfg, manifest)
				if err != nil {
					return err
				}
			}

			if version == "next-build" {
				version, err = modules.NextPatch(cfg, manifest)
				if err != nil {
					return err
				}
				var commitSha string
				if hash, err := getCurrentCommitSha(); err == nil && len(hash) >= 8 {
					commitSha = hash[0:8]
				} else {
					return fmt.Errorf("Using --version=next-build requires a git repository with a commit. Cannot find commit SHA: %w", err)
				}
				version = fmt.Sprintf("%s+%s", version, commitSha)
			}

			version = strings.TrimPrefix(version, "v")
			if isValid := semver.IsValid(fmt.Sprintf("v%s", version)); !isValid {
				return fmt.Errorf("version %q is not a valid semver", version)
			}

			tarballFilename, err := modules.Package(manifest, version, includes)
			if err != nil {
				return err
			}
			fmt.Fprintf(os.Stderr, "Created module package %q\n", tarballFilename)

			tarball, err := os.Open(tarballFilename)
			if err != nil {
				return err
			}
			defer tarball.Close()

			client := api.Client{Config: cfg}
			if err := client.Org(manifest.OrgName).ModuleVersions().Create(manifest.Name, version, tarball); err != nil {
				return err
			}
			fmt.Fprintf(os.Stderr, "Published %s/%s@%s\n", manifest.OrgName, manifest.Name, version)
			fmt.Fprintln(os.Stdout, version)
			return nil
		})
	},
}
View Source
var ModulesRegister = &cli.Command{
	Name:        "register",
	Description: "Registers a module in the Nullstone registry. The information in .nullstone/module.yml will be used as the details for the new module.",
	Usage:       "Register module from .nullstone/module.yml",
	UsageText:   "nullstone modules register",
	Flags:       []cli.Flag{},
	Aliases:     []string{"new"},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			manifest, err := modules.ManifestFromFile(moduleManifestFilename)
			if err != nil {
				return err
			}

			module, err := modules.Register(cfg, manifest)
			if err != nil {
				return err
			}
			fmt.Printf("registered %s/%s\n", module.OrgName, module.Name)
			return nil
		})
	},
}
View Source
var OldEnvFlag = &cli.StringFlag{
	Name:    "env",
	Usage:   `Name of the environment to use for this operation`,
	EnvVars: []string{"NULLSTONE_ENV"},
}
View Source
var OrgFlag = &cli.StringFlag{
	Name:    "org",
	EnvVars: []string{"NULLSTONE_ORG"},
	Usage:   `Nullstone organization name to use for this operation. If this flag is not specified, the nullstone CLI will use ~/.nullstone/<profile>/org file.`,
}

OrgFlag defines a flag that the CLI uses

to contextualize API calls by that organization within Nullstone

The organization takes the following precedence:

`--org` flag
`NULLSTONE_ORG` env var
`~/.nullstone/<profile>/org` file
View Source
var Outputs = func() *cli.Command {
	return &cli.Command{
		Name:        "outputs",
		Description: "Print all the module outputs for a given block and environment. Provide the `--sensitive` flag to include sensitive outputs in the results. You must have proper permissions in order to use the `--sensitive` flag. For less information in an easier to read format, use the `--plain` flag.",
		Usage:       "Retrieve outputs",
		UsageText:   "nullstone outputs [--stack=<stack-name>] --block=<block-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			BlockFlag,
			EnvFlag,
			&cli.BoolFlag{
				Name:  "sensitive",
				Usage: "Include sensitive outputs in the results",
			},
			&cli.BoolFlag{
				Name:  "plain",
				Usage: "Print less information about the outputs in a more readable format",
			},
		},
		Action: func(c *cli.Context) error {
			return BlockWorkspaceAction(c, func(ctx context.Context, cfg api.Config, stack types.Stack, block types.Block, env types.Environment, workspace types.Workspace) error {
				client := api.Client{Config: cfg}
				showSensitive := c.IsSet("sensitive")
				outputs, err := client.WorkspaceOutputs().GetCurrent(stack.Id, workspace.Uid, showSensitive)
				if err != nil {
					return err
				}

				for key, output := range outputs {
					if output.Redacted {
						output.Value = "(hidden)"
						outputs[key] = output
					}
				}

				encoder := json.NewEncoder(os.Stdout)
				encoder.SetIndent("", "  ")
				if c.IsSet("plain") {
					stripped := map[string]any{}
					for key, output := range outputs {
						stripped[key] = output.Value
					}
					encoder.Encode(stripped)
				} else {
					encoder.Encode(outputs)
				}

				return nil
			})
		},
	}
}

Outputs command retrieves outputs from a workspace (block+env)

View Source
var Plan = func() *cli.Command {
	return &cli.Command{
		Name:        "plan",
		Description: "Run a plan for a given block and environment. This will automatically disapprove the plan and is useful for testing what a plan will do.",
		Usage:       "Runs a plan with a disapproval",
		UsageText:   "nullstone plan [--stack=<stack-name>] --block=<block-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			BlockFlag,
			EnvFlag,
			&cli.BoolFlag{
				Name:    "wait",
				Aliases: []string{"w"},
				Usage:   "Wait for the plan to complete and stream the Terraform logs to the console.",
			},
			&cli.StringSliceFlag{
				Name:  "var",
				Usage: "Set variables values for the plan. This can be used to override variables defined in the module.",
			},
			&cli.StringFlag{
				Name:  "module-version",
				Usage: "Run a plan with a specific version of the module.",
			},
		},
		Action: func(c *cli.Context) error {
			varFlags := c.StringSlice("var")
			moduleVersion := c.String("module-version")

			return BlockWorkspaceAction(c, func(ctx context.Context, cfg api.Config, stack types.Stack, block types.Block, env types.Environment, workspace types.Workspace) error {
				if moduleVersion != "" {
					module := types.WorkspaceModuleInput{
						Module:        block.ModuleSource,
						ModuleVersion: moduleVersion,
					}
					err := runs.SetModuleVersion(cfg, workspace, module)
					if err != nil {
						return err
					}
				}

				err := runs.SetConfigVars(cfg, workspace, varFlags)
				if err != nil {
					return err
				}

				f := false
				newRun, err := runs.Create(cfg, workspace, &f, false)
				if err != nil {
					return fmt.Errorf("error creating run: %w", err)
				} else if newRun == nil {
					return fmt.Errorf("unable to create run")
				}
				fmt.Fprintf(os.Stdout, "created plan run %q\n", newRun.Uid)
				fmt.Fprintln(os.Stdout, runs.GetBrowserUrl(cfg, workspace, *newRun))

				if c.IsSet("wait") {
					err := runs.StreamLogs(ctx, cfg, workspace, newRun)
					if err == runs.ErrRunDisapproved {

						return nil
					}
					return err
				}
				return nil
			})
		},
	}
}
View Source
var PodFlag = &cli.StringFlag{
	Name: "pod",
	Usage: `Select a pod to execute the command against.
        When specified, allows you to connect to a specific pod within a replica set.
        This is optional and will connect to a random pod by default.
        This is only used by Kubernetes clusters and determines which pod in the replica to connect.`,
}
View Source
var Profile = &cli.Command{
	Name:      "profile",
	Usage:     "View the current profile and its configuration",
	UsageText: "nullstone profile",
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			fmt.Printf("Profile: %s\n", GetProfile(c))
			fmt.Printf("API Address: %s\n", cfg.BaseAddress)
			if cfg.ApiKey != "" {
				fmt.Printf("API Key: *** redacted ***\n")
			} else {
				fmt.Printf("API Key: (not set)\n")
			}
			fmt.Printf("Is Trace Enabled: %t\n", cfg.IsTraceEnabled)
			fmt.Printf("Org Name: %s\n", cfg.OrgName)
			fmt.Println()
			return nil
		})
	},
}
View Source
var ProfileFlag = &cli.StringFlag{
	Name:    "profile",
	EnvVars: []string{"NULLSTONE_PROFILE"},
	Value:   "default",
	Usage:   "Name of the profile to use for the operation",
}
View Source
var Push = func(providers app.Providers) *cli.Command {
	return &cli.Command{
		Name:        "push",
		Description: "Upload (push) an artifact containing the source for your application. Specify a semver version to associate with the artifact. The version specified can be used in the deploy command to select this artifact.",
		Usage:       "Push artifact",
		UsageText:   "nullstone push [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			OldEnvFlag,
			AppSourceFlag,
			AppVersionFlag,
		},
		Action: func(c *cli.Context) error {
			return AppWorkspaceAction(c, func(ctx context.Context, cfg api.Config, appDetails app.Details) error {
				source, version := c.String("source"), DetectAppVersion(c)
				osWriters := logging.StandardOsWriters{}
				provider := providers.FindFactory(*appDetails.Module)
				if provider == nil {
					return fmt.Errorf("push is not supported for this app")
				}
				return push(ctx, cfg, appDetails, osWriters, provider, source, version)
			})
		},
	}
}

Push command performs a docker push to an authenticated image registry configured against an app/container

View Source
var SetOrg = &cli.Command{
	Name:        "set-org",
	Description: "Most Nullstone CLI commands require a configured nullstone organization to operate. This command will set the organization for the current profile. If you wish to set the organization per command, use the global `--org` flag instead.",
	Usage:       "Set the organization for the CLI",
	UsageText:   `nullstone set-org <org-name>`,
	Flags:       []cli.Flag{},
	Action: func(c *cli.Context) error {
		profile, err := config.LoadProfile(GetProfile(c))
		if err != nil {
			return err
		}

		if c.NArg() != 1 {
			return cli.ShowCommandHelp(c, "set-org")
		}

		orgName := c.Args().Get(0)
		if err := profile.SaveOrg(orgName); err != nil {
			return err
		}
		fmt.Fprintf(os.Stderr, "Organization set to %s for %s profile\n", orgName, profile.Name)
		return nil
	},
}
View Source
var Ssh = func(providers admin.Providers) *cli.Command {
	return &cli.Command{
		Name:        "ssh",
		Description: "SSH into a running app container or virtual machine. Use the `--forward, L` option to forward ports from remote service or hosts.",
		Usage:       "SSH into a running application.",
		UsageText:   "nullstone ssh [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			EnvFlag,
			TaskFlag,
			PodFlag,
			ContainerFlag,
			&cli.StringSliceFlag{
				Name:    "forward",
				Aliases: []string{"L"},
				Usage:   "Use this to forward ports from host to local machine. Format: <local-port>:[<remote-host>]:<remote-port>",
			},
		},
		Action: func(c *cli.Context) error {
			forwards := make([]config.PortForward, 0)
			for _, arg := range c.StringSlice("forward") {
				pf, err := config.ParsePortForward(arg)
				if err != nil {
					return fmt.Errorf("invalid format for --forward/-L: %w", err)
				}
				forwards = append(forwards, pf)
			}

			return AppWorkspaceAction(c, func(ctx context.Context, cfg api.Config, appDetails app.Details) error {
				remoter, err := providers.FindRemoter(logging.StandardOsWriters{}, cfg, appDetails)
				if err != nil {
					return err
				} else if remoter == nil {
					module := appDetails.Module
					platform := strings.TrimSuffix(fmt.Sprintf("%s:%s", module.Platform, module.Subplatform), ":")
					return fmt.Errorf("The Nullstone CLI does not currently support the ssh command for the %q application. (Module = %s/%s, App Category = app/%s, Platform = %s)",
						appDetails.App.Name, module.OrgName, module.Name, module.Subcategory, platform)
				}
				options := admin.RemoteOptions{
					Task:         c.String("task"),
					Pod:          c.String("pod"),
					Container:    c.String("container"),
					PortForwards: forwards,
				}
				return remoter.Ssh(ctx, options)
			})
		},
	}
}
View Source
var StackFlag = &cli.StringFlag{
	Name:    "stack",
	Usage:   "Scope this operation to a specific stack. This is only required if there are multiple blocks/apps with the same name.",
	EnvVars: []string{"NULLSTONE_STACK"},
}
View Source
var StackRequiredFlag = &cli.StringFlag{
	Name:     "stack",
	Usage:    "Name of the stack to use for this operation",
	EnvVars:  []string{"NULLSTONE_STACK"},
	Required: true,
}
View Source
var Stacks = &cli.Command{
	Name:      "stacks",
	Usage:     "View and modify stacks",
	UsageText: "nullstone stacks [subcommand]",
	Subcommands: []*cli.Command{
		StacksList,
		StacksNew,
	},
}
View Source
var StacksList = &cli.Command{
	Name:        "list",
	Description: "Shows a list of the stacks that you have access to. Set the `--detail` flag to show more details about each stack.",
	Usage:       "List stacks",
	UsageText:   "nullstone stacks list",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
			Usage:   "Use this flag to show more details about each stack",
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			client := api.Client{Config: cfg}
			allStacks, err := client.Stacks().List()
			if err != nil {
				return fmt.Errorf("error listing stacks: %w", err)
			}

			if c.IsSet("detail") {
				stackDetails := make([]string, len(allStacks)+1)
				stackDetails[0] = "ID|Name|Description"
				for i, stack := range allStacks {
					stackDetails[i+1] = fmt.Sprintf("%d|%s|%s", stack.Id, stack.Name, stack.Description)
				}
				fmt.Println(columnize.Format(stackDetails, columnize.DefaultConfig()))
			} else {
				for _, stack := range allStacks {
					fmt.Println(stack.Name)
				}
			}

			return nil
		})
	},
}
View Source
var StacksNew = &cli.Command{
	Name:        "new",
	Description: "Creates a new stack with the given name and in the organization configured for the CLI.",
	Usage:       "Create new stack",
	UsageText:   "nullstone stacks new --name=<name> --description=<description>",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:     "name",
			Usage:    "The name of the stack to create. This name must be unique within the organization.",
			Required: true,
		},
		&cli.StringFlag{
			Name:     "description",
			Usage:    "The description of the stack to create.",
			Required: true,
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			client := api.Client{Config: cfg}
			name := c.String("name")
			description := c.String("description")
			stack, err := client.Stacks().Create(&types.Stack{
				Name:        name,
				Description: description,
			})
			if err != nil {
				return fmt.Errorf("error creating stack: %w", err)
			}
			fmt.Printf("created %q stack\n", stack.Name)
			return nil
		})
	},
}
View Source
var Status = func(providers admin.Providers) *cli.Command {
	return &cli.Command{
		Name:        "status",
		Description: "View the status of your application and whether it is starting up, running, stopped, etc. This command shows the status of an application's tasks as well as the health of the load balancer.",
		Usage:       "Application Status",
		UsageText:   "nullstone status [--stack=<stack-name>] --app=<app-name> [--env=<env-name>] [options]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			EnvOptionalFlag,
			AppVersionFlag,
			&cli.BoolFlag{
				Name:    "watch",
				Aliases: []string{"w"},
				Usage:   "Pass this flag in order to watch status updates in real time. Changes will be automatically displayed as they occur.",
			},
		},
		Action: func(c *cli.Context) error {
			watchInterval := -1 * time.Second
			if c.IsSet("watch") {
				watchInterval = defaultWatchInterval
			}

			return ProfileAction(c, func(cfg api.Config) error {
				return ParseAppEnv(c, false, func(stackName, appName, envName string) error {
					return CancellableAction(func(ctx context.Context) error {
						if envName == "" {
							return appStatus(ctx, cfg, providers, watchInterval, stackName, appName)
						} else {
							return appEnvStatus(ctx, cfg, providers, watchInterval, stackName, appName, envName)
						}
					})
				})
			})
		},
	}
}
View Source
var TaskFlag = &cli.StringFlag{
	Name: "task",
	Usage: `Select a specific task to execute the command against.
		This is optional and by default will connect to a random task.
        This is only used by ECS and determines which task to connect.`,
}
View Source
var Up = func() *cli.Command {
	return &cli.Command{
		Name:        "up",
		Description: "Launches the infrastructure for the given block/environment and it's dependencies.",
		Usage:       "Provisions the block and all of its dependencies",
		UsageText:   "nullstone up [--stack=<stack-name>] --block=<block-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			BlockFlag,
			EnvFlag,
			&cli.BoolFlag{
				Name:    "wait",
				Aliases: []string{"w"},
				Usage:   "Wait for the launch to complete and stream the Terraform logs to the console.",
			},
			&cli.StringSliceFlag{
				Name:  "var",
				Usage: "Set variables values for the plan. This can be used to override variables defined in the module.",
			},
		},
		Action: func(c *cli.Context) error {
			varFlags := c.StringSlice("var")

			return BlockWorkspaceAction(c, func(ctx context.Context, cfg api.Config, stack types.Stack, block types.Block, env types.Environment, workspace types.Workspace) error {
				if workspace.Status == types.WorkspaceStatusProvisioned {
					fmt.Println("workspace is already provisioned")
					return nil
				}

				err := runs.SetConfigVars(cfg, workspace, varFlags)
				if err != nil {
					return err
				}

				t := true
				newRun, err := runs.Create(cfg, workspace, &t, false)
				if err != nil {
					return fmt.Errorf("error creating run: %w", err)
				} else if newRun == nil {
					return fmt.Errorf("unable to create run")
				}
				fmt.Printf("created run %q\n", newRun.Uid)
				fmt.Fprintln(os.Stdout, runs.GetBrowserUrl(cfg, workspace, *newRun))

				if c.IsSet("wait") {
					return runs.StreamLogs(ctx, cfg, workspace, newRun)
				}
				return nil
			})
		},
	}
}
View Source
var Workspaces = &cli.Command{
	Name:      "workspaces",
	Usage:     "View and modify workspaces",
	UsageText: "nullstone workspaces [subcommand]",
	Subcommands: []*cli.Command{
		WorkspacesSelect,
	},
}
View Source
var WorkspacesSelect = &cli.Command{
	Name:        "select",
	Description: "Sync a given workspace's state with the current directory. Running this command will allow you to run terraform plans/applies locally against the selected workspace.",
	Usage:       "Select workspace",
	UsageText:   "nullstone workspaces select [--stack=<stack>] --block=<block> --env=<env>",
	Flags: []cli.Flag{
		StackFlag,
		&cli.StringFlag{
			Name:     "block",
			Usage:    "Name of the block to use for this operation",
			Required: true,
		},
		&cli.StringFlag{
			Name:     "env",
			Usage:    `Name of the environment to use for this operation`,
			Required: true,
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			if !tfconfig.IsCredsConfigured(cfg) {
				if err := tfconfig.ConfigCreds(cfg); err != nil {
					fmt.Printf("Warning: unable to configure Terraform-based credentials with Nullstone servers: %s\n", err)
				} else {
					fmt.Println("Configured Terraform-based credentials with Nullstone servers.")
				}
			}

			client := api.Client{Config: cfg}
			stackName := c.String("stack")
			blockName := c.String("block")
			envName := c.String("env")
			sbe, err := find.StackBlockEnvByName(cfg, stackName, blockName, envName)
			if err != nil {
				return err
			}

			targetWorkspace := workspaces.Manifest{
				OrgName:     cfg.OrgName,
				StackId:     sbe.Stack.Id,
				StackName:   sbe.Stack.Name,
				BlockId:     sbe.Block.Id,
				BlockName:   sbe.Block.Name,
				BlockRef:    sbe.Block.Reference,
				EnvId:       sbe.Env.Id,
				EnvName:     sbe.Env.Name,
				Connections: workspaces.ManifestConnections{},
			}
			workspace, err := client.Workspaces().Get(targetWorkspace.StackId, targetWorkspace.BlockId, targetWorkspace.EnvId)
			if err != nil {
				return err
			} else if workspace == nil {
				return fmt.Errorf("could not find workspace (stack=%s, block=%s, env=%s)", stackName, blockName, envName)
			}
			targetWorkspace.WorkspaceUid = workspace.Uid.String()

			runConfig, err := workspaces.GetRunConfig(cfg, targetWorkspace)
			if err != nil {
				return fmt.Errorf("could not retreive current workspace configuration: %w", err)
			}
			manualConnections, err := surveyMissingConnections(cfg, targetWorkspace.StackName, runConfig)
			if err != nil {
				return err
			}
			for name, conn := range manualConnections {
				targetWorkspace.Connections[name] = workspaces.ManifestConnectionTarget{
					StackId:   conn.Reference.StackId,
					BlockId:   conn.Reference.BlockId,
					BlockName: conn.Reference.BlockName,
					EnvId:     conn.Reference.EnvId,
				}
			}

			return CancellableAction(func(ctx context.Context) error {
				return workspaces.Select(ctx, cfg, targetWorkspace, runConfig)
			})
		})
	},
}

Functions

func AppWorkspaceAction added in v0.0.67

func AppWorkspaceAction(c *cli.Context, fn AppWorkspaceFn) error

func BlockWorkspaceAction added in v0.0.68

func BlockWorkspaceAction(c *cli.Context, fn BlockWorkspaceActionFn) error

func CancellableAction added in v0.0.26

func CancellableAction(fn func(ctx context.Context) error) error

func CreateDeploy added in v0.0.67

func CreateDeploy(nsConfig api.Config, appDetails app.Details, version string) (*types.Deploy, error)

func DetectAppVersion added in v0.0.28

func DetectAppVersion(c *cli.Context) string

func FindAppDetails added in v0.0.67

func FindAppDetails(cfg api.Config, appName, stackName, envName string) (app.Details, error)

FindAppDetails retrieves the app, env, and workspace stackName is optional -- If multiple apps are found, this will return an error

func GetApp added in v0.0.43

func GetApp(c *cli.Context) string

func GetEnvironment added in v0.0.43

func GetEnvironment(c *cli.Context) string

func GetOrg added in v0.0.4

func GetOrg(c *cli.Context, profile config.Profile) string

func GetProfile added in v0.0.4

func GetProfile(c *cli.Context) string

func ParseAppEnv added in v0.0.43

func ParseAppEnv(c *cli.Context, isEnvRequired bool, fn ParseAppEnvFn) error

func ProfileAction added in v0.0.26

func ProfileAction(c *cli.Context, fn ProfileFn) error

func SetupProfileCmd added in v0.0.7

func SetupProfileCmd(c *cli.Context) (*config.Profile, api.Config, error)

func WatchAction added in v0.0.26

func WatchAction(ctx context.Context, watchInterval time.Duration, fn func(w io.Writer) error) error

Types

type AppWorkspaceFn added in v0.0.67

type AppWorkspaceFn func(ctx context.Context, cfg api.Config, appDetails app.Details) error

type AppWorkspaceInfo added in v0.0.26

type AppWorkspaceInfo struct {
	AppDetails app.Details
	Status     string
	Version    string
}

type BlockWorkspaceActionFn added in v0.0.68

type BlockWorkspaceActionFn func(ctx context.Context, cfg api.Config, stack types.Stack, block types.Block, env types.Environment, workspace types.Workspace) error

type NsStatus added in v0.0.26

type NsStatus struct {
	Config api.Config
}

func (NsStatus) GetAppWorkspaceInfo added in v0.0.26

func (s NsStatus) GetAppWorkspaceInfo(application *types.Application, env *types.Environment) (AppWorkspaceInfo, error)

type ParseAppEnvFn added in v0.0.43

type ParseAppEnvFn func(stackName, appName, envName string) error

type ProfileFn added in v0.0.26

type ProfileFn func(cfg api.Config) error

type TableBuffer added in v0.0.26

type TableBuffer struct {
	Fields   []string
	HasField map[string]bool
	Data     []map[string]interface{}
}

TableBuffer builds a table of data to display on the terminal The TableBuffer guarantees safe merging of rows with potentially different field names Example: If a user is migrating an app from container to serverless,

it's possible that the infrastructure has not fully propagated

func (*TableBuffer) AddFields added in v0.0.26

func (b *TableBuffer) AddFields(fields ...string)

func (*TableBuffer) AddRow added in v0.0.26

func (b *TableBuffer) AddRow(data map[string]interface{})

func (*TableBuffer) String added in v0.0.26

func (b *TableBuffer) String() string

func (*TableBuffer) Values added in v0.0.26

func (b *TableBuffer) Values() [][]string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL