cmd

package
v0.0.78 Latest Latest
Warning

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

Go to latest
Published: Aug 31, 2022 License: MIT Imports: 34 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	AddressFlag = &cli.StringFlag{
		Name:  "address",
		Value: api.DefaultAddress,
		Usage: "Nullstone API Address",
	}
	ApiKeyFlag = &cli.StringFlag{
		Name:  "api-key",
		Value: "",
		Usage: "Nullstone API Key",
	}
)
View Source
var AppFlag = &cli.StringFlag{
	Name:    "app",
	Usage:   "Set the application name.",
	EnvVars: []string{"NULLSTONE_APP"},
}
View Source
var AppSourceFlag = &cli.StringFlag{
	Name: "source",
	Usage: `The source artifact to push.
       app/container: This is the docker image to push. This follows the same syntax as 'docker push NAME[:TAG]'.
       app/serverless: This is a .zip archive to push.`,
	Required: true,
}
View Source
var AppVersionFlag = &cli.StringFlag{
	Name: "version",
	Usage: `Push/Deploy the artifact with this version.
       If not specified, will retrieve short sha from your latest commit.
       app/container: If specified, will push the docker image with version as the image tag. Otherwise, uses source tag.
       app/serverless: This is required to upload the artifact.`,
}
View Source
var Apply = func() *cli.Command {
	return &cli.Command{
		Name:      "apply",
		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:   "Stream the Terraform logs while waiting for Nullstone to run the apply.",
			},
			&cli.BoolFlag{
				Name:  "auto-approve",
				Usage: "Auto-approve any changes made in Terraform",
			},
			&cli.StringSliceFlag{
				Name:  "var",
				Usage: "Set variable values when issuing `apply`",
			},
			&cli.StringFlag{
				Name:  "module-version",
				Usage: "Use a specific module version to run the 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 {
				moduleSourceOverride := ""
				if moduleVersion != "" {
					moduleSourceOverride = fmt.Sprintf("%s@%s", block.ModuleSource, moduleVersion)
				}
				newRunConfig, err := runs.GetPromotion(cfg, workspace, moduleSourceOverride)
				if err != nil {
					return fmt.Errorf("error getting run configuration for apply: %w", err)
				}

				skipped, err := runs.SetRunConfigVars(newRunConfig, varFlags)
				if len(skipped) > 0 {
					fmt.Printf("[Warning] The following variables were skipped because they don't exist in the module: %s\n\n", strings.Join(skipped, ", "))
				}
				if err != nil {
					return err
				}

				newRun, err := runs.Create(cfg, workspace, newRunConfig, 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",
	Usage:     "List applications",
	UsageText: "nullstone apps list",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
		},
	},
	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
					}
					appDetails[i+1] = fmt.Sprintf("%d|%s|%s|%s|%s|%s|%s|%s", app.Id, app.Name, app.Reference, appCategory, appType, app.ModuleSource, app.StackName, 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:    "Set the block name.",
	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",
	Usage:     "List blocks",
	UsageText: "nullstone blocks list --stack=<stack>",
	Flags: []cli.Flag{
		StackRequiredFlag,
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
		},
	},
	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, block.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",
	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,
		},
		&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: "Map the connection name on the module to the block name in the stack. 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
			}
			sort.Sort(sort.Reverse(module.Versions))
			var latestModuleVersion *types.ModuleVersion
			if len(module.Versions) > 0 {
				latestModuleVersion = &module.Versions[0]
			}

			connections, parentBlocks, err := mapConnectionsToTargets(cfg, stack, connectionSlice)
			if err != nil {
				return err
			}
			if err := validateConnections(latestModuleVersion, 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,
				ParentBlocks:        parentBlocks,
			}
			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",
	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 Deploy = func(providers app.Providers) *cli.Command {
	return &cli.Command{
		Name:      "deploy",
		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"},
			},
		},
		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")
				}

				deploy, err := CreateDeploy(cfg, appDetails, version)
				if err != nil {
					return err
				}
				return streamDeployLogs(ctx, cfg, *deploy, wait)
			})
		},
	}
}
View Source
var EnvFlag = &cli.StringFlag{
	Name:     "env",
	Usage:    `Set the environment name.`,
	EnvVars:  []string{"NULLSTONE_ENV"},
	Required: true,
}
View Source
var EnvOptionalFlag = &cli.StringFlag{
	Name:     "env",
	Usage:    `Set the environment name.`,
	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,
	},
}
View Source
var EnvsList = &cli.Command{
	Name:      "list",
	Usage:     "List environments",
	UsageText: "nullstone envs list --stack=<stack-name>",
	Flags: []cli.Flag{
		StackRequiredFlag,
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
		},
	},
	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 {
				return envs[i].PipelineOrder < envs[i].PipelineOrder
			})

			if c.IsSet("detail") {
				envDetails := make([]string, len(envs)+1)
				envDetails[0] = "ID|Name"
				for i, env := range envs {
					envDetails[i+1] = fmt.Sprintf("%d|%s", env.Id, env.Name)
				}
				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",
	Usage:     "Create new environment",
	UsageText: "nullstone envs new --name=<name> --stack=<stack> --provider=<provider>",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "name", Required: true},
		StackFlag,
		&cli.StringFlag{Name: "provider", Required: true},
		&cli.StringFlag{Name: "region"},
		&cli.StringFlag{Name: "zone"},
	},
	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")

			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)
			}

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

			pc := types.ProviderConfig{}
			switch provider.ProviderType {
			case "aws":
				pc.Aws = &types.AwsProviderConfig{
					ProviderName: provider.Name,
					Region:       c.String("region"),
				}
				if pc.Aws.Region == "" {
					pc.Aws.Region = awsDefaultRegion
				}
			case "gcp":
				pc.Gcp = &types.GcpProviderConfig{
					ProviderName: provider.Name,
					Region:       c.String("region"),
					Zone:         c.String("zone"),
				}
				if pc.Gcp.Region == "" || pc.Gcp.Zone == "" {
					pc.Gcp.Region = gcpDefaultRegion
					pc.Gcp.Zone = gcpDefaultZone
				}
			default:
				return fmt.Errorf("CLI does not support provider type %q yet", provider.ProviderType)
			}

			env, err := client.Environments().Create(stack.Id, &types.Environment{
				Name:           name,
				ProviderConfig: pc,
			})
			if err != nil {
				return fmt.Errorf("error creating stack: %w", err)
			}
			fmt.Printf("created %q environment\n", env.Name)
			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",
		Usage:     "Execute command on running service. Defaults command to '/bin/sh' which acts as opening a shell to the running container.",
		UsageText: "nullstone exec [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options] [command]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			EnvFlag,
			TaskFlag,
		},
		Action: func(c *cli.Context) error {
			task := c.String("task")
			cmd := "/bin/sh"
			if c.Args().Len() >= 1 {
				cmd = c.Args().Get(c.Args().Len() - 1)
			}

			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
				}
				return remoter.Exec(ctx, task, cmd)
			})
		},
	}
}
View Source
var Launch = func(providers app.Providers) *cli.Command {
	return &cli.Command{
		Name:      "launch",
		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
				}

				deploy, err := CreateDeploy(cfg, appDetails, version)
				if err != nil {
					return err
				}
				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",
		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.
      `,
			},
			&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, 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
			}
			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",
	Usage:     "Generate new module manifest (and optionally register)",
	UsageText: "nullstone modules generate [--register]",
	Flags: []cli.Flag{
		&cli.BoolFlag{Name: "register"},
	},
	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",
	Usage:     "Package a module",
	UsageText: "nullstone modules package",
	Flags:     []cli.Flag{},
	Action: func(c *cli.Context) error {

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

		tarballFilename, err := modules.Package(manifest, "")
		if err == nil {
			fmt.Printf("created module package %q\n", tarballFilename)
		}
		return err
	},
}
View Source
var ModulesPublish = &cli.Command{
	Name:      "publish",
	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,
		},
	},
	Action: func(c *cli.Context) error {
		return ProfileAction(c, func(cfg api.Config) error {
			version := c.String("version")

			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)
			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",
	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:   `Set the environment name.`,
	EnvVars: []string{"NULLSTONE_ENV"},
}
View Source
var OrgFlag = &cli.StringFlag{
	Name:    "org",
	EnvVars: []string{"NULLSTONE_ORG"},
	Usage:   `Nullstone organization name used to contextualize API calls. 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",
		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: "plain",
			},
		},
		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}
				outputs, err := client.WorkspaceOutputs().GetLatest(stack.Id, block.Id, env.Id)
				if err != nil {
					return err
				} else if outputs == nil {
					outputs = &types.Outputs{}
				}

				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",
		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:   "Stream the Terraform logs while waiting for Nullstone to run the plan.",
			},
			&cli.StringSliceFlag{
				Name:  "var",
				Usage: "Set variable values when issuing `plan`",
			},
			&cli.StringFlag{
				Name:  "module-version",
				Usage: "Use a specific module version to run the plan.",
			},
		},
		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 {
				moduleSourceOverride := ""
				if moduleVersion != "" {
					moduleSourceOverride = fmt.Sprintf("%s@%s", block.ModuleSource, moduleVersion)
				}
				newRunConfig, err := runs.GetPromotion(cfg, workspace, moduleSourceOverride)
				if err != nil {
					return fmt.Errorf("error getting run configuration for plan: %w", err)
				}

				skipped, err := runs.SetRunConfigVars(newRunConfig, varFlags)
				if len(skipped) > 0 {
					fmt.Printf("[Warning] The following variables were skipped because they don't exist in the module: %s\n\n", strings.Join(skipped, ", "))
				}
				if err != nil {
					return err
				}

				f := false
				newRun, err := runs.Create(cfg, workspace, newRunConfig, &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 ProfileFlag = &cli.StringFlag{
	Name:    "profile",
	EnvVars: []string{"NULLSTONE_PROFILE"},
	Value:   "default",
	Usage:   "Name of profile",
}
View Source
var Push = func(providers app.Providers) *cli.Command {
	return &cli.Command{
		Name:      "push",
		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",
	Usage: "Set the organization for the CLI",
	UsageText: `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.`,
	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",
		Usage:     "SSH into a running service. Use to forward ports from remote service or hosts.",
		UsageText: "nullstone ssh [--stack=<stack-name>] --app=<app-name> --env=<env-name> [options]",
		Flags: []cli.Flag{
			StackFlag,
			AppFlag,
			EnvFlag,
			TaskFlag,
			&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 {
			task := c.String("task")

			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
				}
				return remoter.Ssh(ctx, task, forwards)
			})
		},
	}
}
View Source
var StackFlag = &cli.StringFlag{
	Name: "stack",
	Usage: `Set the stack name that owns the app/block.
       This is only required if multiple apps/blocks have the same name.`,
	EnvVars: []string{"NULLSTONE_STACK"},
}
View Source
var StackRequiredFlag = &cli.StringFlag{
	Name: "stack",
	Usage: `Set the stack name that owns the app/block.
       This is only required if multiple apps/blocks have the same name.`,
	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",
	Usage:     "List stacks",
	UsageText: "nullstone stacks list",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "detail",
			Aliases: []string{"d"},
		},
	},
	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",
	Usage:     "Create new stack",
	UsageText: "nullstone stacks new --name=<name> --description=<description>",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "name", Required: true},
		&cli.StringFlag{Name: "description", 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",
		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"},
			},
		},
		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: `Optionally, specify the task/replica to execute the command against.
       If not specified, this will connect to a random task/replica.
       If using Kubernetes, this will select which replica of the pod to connect.
       If using ECS, this will select which task of the service to connect.`,
}
View Source
var Up = func() *cli.Command {
	return &cli.Command{
		Name:      "up",
		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 Nullstone to fully provision the workspace.",
			},
			&cli.StringSliceFlag{
				Name:  "var",
				Usage: "Set variable values when issuing `up`",
			},
		},
		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
				}

				newRunConfig, err := runs.GetPromotion(cfg, workspace, "")
				if err != nil {
					return err
				}
				skipped, err := runs.SetRunConfigVars(newRunConfig, varFlags)
				if len(skipped) > 0 {
					fmt.Printf("[Warning] The following variables were skipped because they don't exist in the module: %s\n\n", strings.Join(skipped, ", "))
				}
				if err != nil {
					return err
				}

				t := true
				newRun, err := runs.Create(cfg, workspace, newRunConfig, &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",
	Usage:     "Select workspace",
	UsageText: "nullstone workspaces select [--stack=<stack>] --block=<block> --env=<env>",
	Flags: []cli.Flag{
		StackFlag,
		&cli.StringFlag{
			Name:     "block",
			Required: true,
		},
		&cli.StringFlag{
			Name:     "env",
			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