Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add command for previewing deployments and add pre-preview to deployments #848

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Documentation for all available commands:
(alias: nitric down)
- nitric stack list : List all stacks in the project
- nitric stack new [stackName] [providerName] : Create a new Nitric stack
- nitric stack preview [-s stack] : Preview the updates for a deployed stack
(alias: nitric preview)
- nitric stack update [-s stack] : Create or update a deployed stack
(alias: nitric up)
- nitric start : Run nitric services locally for development and testing
Expand Down
304 changes: 302 additions & 2 deletions cmd/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ import (
"github.com/nitrictech/cli/pkg/view/tui/commands/build"
stack_down "github.com/nitrictech/cli/pkg/view/tui/commands/stack/down"
stack_new "github.com/nitrictech/cli/pkg/view/tui/commands/stack/new"
stack_preview "github.com/nitrictech/cli/pkg/view/tui/commands/stack/preview"
stack_select "github.com/nitrictech/cli/pkg/view/tui/commands/stack/select"
stack_up "github.com/nitrictech/cli/pkg/view/tui/commands/stack/up"
"github.com/nitrictech/cli/pkg/view/tui/components/list"
"github.com/nitrictech/cli/pkg/view/tui/components/listprompt"
"github.com/nitrictech/cli/pkg/view/tui/components/view"
"github.com/nitrictech/cli/pkg/view/tui/teax"
deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1"
Expand All @@ -56,6 +58,7 @@ var (
noBuilder bool
forceNewStack bool
envFile string
skipPreview bool
)

var stackCmd = &cobra.Command{
Expand Down Expand Up @@ -303,13 +306,39 @@ var stackUpdateCmd = &cobra.Command{
attributesStruct, err := structpb.NewStruct(attributes)
tui.CheckErr(err)

if !skipPreview && !isNonInteractive() {
eventChan, errorChan := deploymentClient.Preview(&deploymentspb.DeploymentPreviewRequest{
Spec: spec,
Attributes: attributesStruct,
Interactive: true,
})

stackPreview := stack_preview.New(stackConfig.Provider, stackConfig.Name, eventChan, providerStdout, errorChan)
_, err = teax.NewProgram(stackPreview).Run()

confirmModel := stack_preview.NewConfirmDeployment(listprompt.ListPromptArgs{
Items: list.StringsToListItems([]string{"confirm deployment", "cancel"}),
Tag: "deploy",
Prompt: "Would you like to perform the deployment?",
})

selection, err := teax.NewProgram(confirmModel).Run()
tui.CheckErr(err)

stackSelection = selection.(stack_preview.ConfirmDeploymentModel).Choice()
if stackSelection == "cancel" {
fmt.Println("Cancelled")
return
}
}

eventChan, errorChan := deploymentClient.Up(&deploymentspb.DeploymentUpRequest{
Spec: spec,
Attributes: attributesStruct,
Interactive: true,
})

// Step 5b. Communicate with server to share progress of ...
// Step 5d. Communicate with server to share progress of update
if isNonInteractive() {
providerErrorDetected := false

Expand Down Expand Up @@ -362,7 +391,7 @@ var stackUpdateCmd = &cobra.Command{
}
} else {
// interactive environment
// Step 5c. Start the stack up view
// Step 5e. Start the stack up view
stackUp := stack_up.New(stackConfig.Provider, stackConfig.Name, eventChan, providerStdout, errorChan)
_, err = teax.NewProgram(stackUp).Run()
tui.CheckErr(err)
Expand Down Expand Up @@ -562,6 +591,268 @@ nitric stack down -s aws -y`,
Args: cobra.ExactArgs(0),
}

var stackPreviewCommand = &cobra.Command{
Use: "preview [-s stack]",
Short: "Preview the updates for a deployed stack",
Long: `Preview the updates a deployed stack`,
Example: `nitric stack preview -s aws`,
Run: func(cmd *cobra.Command, args []string) {
fs := afero.NewOsFs()

stackFiles, err := stack.GetAllStackFiles(fs)
tui.CheckErr(err)

if len(stackFiles) == 0 {
tui.CheckErr(fmt.Errorf("no stacks found in project, to create a new one run `nitric stack new`"))
}

// Step 0. Get the stack file, or prompt if more than 1.
stackSelection := stackFlag

if isNonInteractive() {
if len(stackFiles) > 1 && stackSelection == "" {
tui.CheckErr(fmt.Errorf("multiple stacks found in project, please specify one with -s"))
}
}

if stackSelection == "" {
if len(stackFiles) > 1 {
stackList := make([]list.ListItem, len(stackFiles))

for i, stackFile := range stackFiles {
stackName, err := stack.GetStackNameFromFileName(stackFile)
tui.CheckErr(err)
stackConfig, err := stack.ConfigFromName[map[string]any](fs, stackName)
tui.CheckErr(err)
stackList[i] = stack_select.StackListItem{
Name: stackConfig.Name,
Provider: stackConfig.Provider,
}
}

promptModel := stack_select.New(stack_select.Args{
Prompt: "Which stack would you like to preview?",
StackList: stackList,
})

selection, err := teax.NewProgram(promptModel).Run()
tui.CheckErr(err)
stackSelection = selection.(stack_select.Model).Choice()
if stackSelection == "" {
return
}
} else {
stackSelection, err = stack.GetStackNameFromFileName(stackFiles[0])
tui.CheckErr(err)
}
}

stackConfig, err := stack.ConfigFromName[map[string]any](fs, stackSelection)
tui.CheckErr(err)

if !isNonInteractive() {
_ = pulumi.EnsurePulumiPassphrase(fs)
}

// print provider version check
update.PrintOutdatedProviderWarning(stackConfig.Provider)

proj, err := project.FromFile(fs, "")
tui.CheckErr(err)

// Step 0a. Locate/Download provider where applicable.
prov, err := provider.NewProvider(stackConfig.Provider, proj, fs)
tui.CheckErr(err)

err = prov.Install()
tui.CheckErr(err)

// Build the Project's Services (Containers)
buildUpdates, err := proj.BuildServices(fs, !noBuilder)
tui.CheckErr(err)

batchBuildUpdates, err := proj.BuildBatches(fs, !noBuilder)
tui.CheckErr(err)

allBuildUpdates := lo.FanIn(10, buildUpdates, batchBuildUpdates)

if isNonInteractive() {
fmt.Println("building project services")
for _, service := range proj.GetServices() {
fmt.Printf("service matched '%s', auto-naming this service '%s'\n", service.GetFilePath(), service.Name)
}

// non-interactive environment
for update := range allBuildUpdates {
for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") {
fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line)
}
}
} else {
prog := teax.NewProgram(build.NewModel(allBuildUpdates, "Building Services"))
// blocks but quits once the above updates channel is closed by the build process
buildModel, err := prog.Run()
tui.CheckErr(err)
if buildModel.(build.Model).Err != nil {
tui.CheckErr(fmt.Errorf("error building services"))
}
}

// Step 2. Start the collectors and containers (respectively in pairs)
// Step 3. Merge requirements from collectors into a specification
serviceRequirements, err := proj.CollectServicesRequirements()
tui.CheckErr(err)

batchRequirements, err := proj.CollectBatchRequirements()
tui.CheckErr(err)

additionalEnvFiles := []string{}

if envFile != "" {
additionalEnvFiles = append(additionalEnvFiles, envFile)
}

envVariables, err := env.ReadLocalEnv(additionalEnvFiles...)
if err != nil && os.IsNotExist(err) {
if !os.IsNotExist(err) {
tui.CheckErr(err)
}
// If it doesn't exist set blank
envVariables = map[string]string{}
}

// Allow Beta providers to be run if 'beta-providers' is enabled in preview flags
if slices.Contains(proj.Preview, preview.Feature_BetaProviders) {
envVariables["NITRIC_BETA_PROVIDERS"] = "true"
}

spec, err := collector.ServiceRequirementsToSpec(proj.Name, envVariables, serviceRequirements, batchRequirements)
tui.CheckErr(err)

migrationImageContexts, err := collector.GetMigrationImageBuildContexts(serviceRequirements, batchRequirements, fs)
tui.CheckErr(err)
// Build images from contexts and provide updates on the builds

if len(migrationImageContexts) > 0 {
migrationBuildUpdates, err := project.BuildMigrationImages(fs, migrationImageContexts, !noBuilder)
tui.CheckErr(err)

if isNonInteractive() {
fmt.Println("building project migration images")
// non-interactive environment
for update := range migrationBuildUpdates {
for _, line := range strings.Split(strings.TrimSuffix(update.Message, "\n"), "\n") {
fmt.Printf("%s [%s]: %s\n", update.ServiceName, update.Status, line)
}
}
} else {
prog := teax.NewProgram(build.NewModel(migrationBuildUpdates, "Building Database Migrations"))
// blocks but quits once the above updates channel is closed by the build process
buildModel, err := prog.Run()
tui.CheckErr(err)
if buildModel.(build.Model).Err != nil {
tui.CheckErr(fmt.Errorf("error building services"))
}
}
}

providerStdout := make(chan string)

// Step 4. Start the deployment provider server
providerAddress, err := prov.Start(&provider.StartOptions{
Env: envVariables,
StdOut: providerStdout,
StdErr: providerStdout,
})
tui.CheckErr(err)
defer func() {
err := prov.Stop()
tui.CheckErr(err)
}()

// Step 5a. Send specification to provider for deployment
deploymentClient := provider.NewDeploymentClient(providerAddress, true)

attributes := map[string]interface{}{}

attributes["stack"] = stackConfig.Name
attributes["project"] = proj.Name

for k, v := range stackConfig.Config {
attributes[k] = v
}

attributesStruct, err := structpb.NewStruct(attributes)
tui.CheckErr(err)

eventChan, errorChan := deploymentClient.Preview(&deploymentspb.DeploymentPreviewRequest{
Spec: spec,
Attributes: attributesStruct,
Interactive: true,
})

// Step 5b. Communicate with server to share progress of ...
if isNonInteractive() {
providerErrorDetected := false

fmt.Printf("Previewing %s stack with provider %s\n", stackConfig.Name, stackConfig.Provider)
go func() {
for update := range errorChan {
fmt.Printf("Error: %s\n", update)
providerErrorDetected = true
}
}()

go func() {
for outMessage := range providerStdout {
fmt.Printf("%s: %s\n", stackConfig.Provider, outMessage)
}
}()

// non-interactive environment
for update := range eventChan {
switch content := update.Content.(type) {
case *deploymentspb.DeploymentPreviewEvent_Message:
fmt.Printf("%s\n", content.Message)
case *deploymentspb.DeploymentPreviewEvent_Update:
updateResType := ""
updateResName := ""
if content.Update.Id != nil {
updateResType = content.Update.Id.Type.String()
updateResName = content.Update.Id.Name
}

if updateResType == "" {
updateResType = "Stack"
}
if updateResName == "" {
updateResName = stackConfig.Name
}
if content.Update.SubResource != "" {
updateResName = fmt.Sprintf("%s:%s", updateResName, content.Update.SubResource)
}

fmt.Printf("%s:%s [%s]:%s %s\n", updateResType, updateResName, content.Update.Action, content.Update.Status, content.Update.Message)
case *deploymentspb.DeploymentPreviewEvent_Result:
fmt.Printf("\nResult: %s\n", content.Result.GetText())
}
}

// ensure the process exits with a non-zero status code after all messages are processed
if providerErrorDetected {
os.Exit(1)
}
} else {
// interactive environment
// Step 5c. Start the stack preview view
stackPreview := stack_preview.New(stackConfig.Provider, stackConfig.Name, eventChan, providerStdout, errorChan)
_, err = teax.NewProgram(stackPreview).Run()
}
},
Args: cobra.MinimumNArgs(0),
Aliases: []string{"preview"},
}

var stackListCmd = &cobra.Command{
Use: "list",
Short: "List all stacks in the project",
Expand Down Expand Up @@ -635,13 +926,21 @@ func init() {
stackUpdateCmd.Flags().BoolVarP(&noBuilder, "no-builder", "", false, "don't create a buildx container")
stackUpdateCmd.Flags().StringVarP(&envFile, "env-file", "e", "", "--env-file config/.my-env")
stackUpdateCmd.Flags().BoolVarP(&forceStack, "force", "f", false, "force override previous deployment")
stackUpdateCmd.Flags().BoolVarP(&skipPreview, "skip-preview", "", false, "skips the preview step of the deployment")
tui.CheckErr(AddOptions(stackUpdateCmd, false))

// Delete Stack (Down)
stackCmd.AddCommand(tui.AddDependencyCheck(stackDeleteCmd))
stackDeleteCmd.Flags().BoolVarP(&confirmDown, "yes", "y", false, "confirm the destruction of the stack")
tui.CheckErr(AddOptions(stackDeleteCmd, false))

// Preview Stack (Preview)
stackCmd.AddCommand(tui.AddDependencyCheck(stackPreviewCommand, tui.RequireContainerBuilder))
stackPreviewCommand.Flags().BoolVarP(&noBuilder, "no-builder", "", false, "don't create a buildx container")
stackPreviewCommand.Flags().StringVarP(&envFile, "env-file", "e", "", "--env-file config/.my-env")
stackPreviewCommand.Flags().BoolVarP(&forceStack, "force", "f", false, "force override previous deployment")
tui.CheckErr(AddOptions(stackPreviewCommand, false))

// List Stacks
stackCmd.AddCommand(stackListCmd)

Expand All @@ -650,4 +949,5 @@ func init() {

addAlias("stack update", "up", true)
addAlias("stack down", "down", true)
addAlias("stack preview", "preview", true)
}
Loading
Loading