command_stack

package
v0.33.0 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ApplyCmd = &cli.Command{
	Name:        "apply",
	Usage:       "Update an existing stack definition",
	Description: "Update an existing stack definition from a TOML or JSON file.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "file",
			Usage:    "Path to the TOML or JSON definition file",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		req, err := loadStackDef(ctx, cmd.GetStringArg("file"), client)
		if err != nil {
			fmt.Println("Error reading definition:", err)
			os.Exit(1)
		}

		existing, err := client.GetStackDefinitionByName(ctx, req.Name)
		if err != nil {
			fmt.Println("Error checking for existing definition:", err)
			os.Exit(1)
		}
		if existing == nil {
			fmt.Printf("Stack definition %q not found. Use 'create-def' to create it.\n", req.Name)
			os.Exit(1)
		}

		_, err = client.UpdateStackDefinition(ctx, existing.Id, req)
		if err != nil {
			fmt.Println("Error updating stack definition:", err)
			os.Exit(1)
		}

		fmt.Printf("Stack definition %q updated.\n", req.Name)
		return nil
	},
}
View Source
var CreateCmd = &cli.Command{
	Name:        "create",
	Usage:       "Create spaces from a stack definition",
	Description: "Create spaces from a stack definition, prefixed and grouped under a stack name.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "definition",
			Usage:    "Name of the stack definition to use",
			Required: true,
		},
		&cli.StringArg{
			Name:     "prefix",
			Usage:    "Prefix for space names (spaces are named prefix-key)",
			Required: true,
		},
		&cli.StringArg{
			Name:     "name",
			Usage:    "Stack name to group spaces under (defaults to prefix)",
			Required: false,
		},
	},
	Run: func(ctx context.Context, cmd *cli.Command) error {
		defName := cmd.GetStringArg("definition")
		prefix := cmd.GetStringArg("prefix")
		stackName := cmd.GetStringArg("name")
		if stackName == "" {
			stackName = prefix
		}

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		def, err := client.GetStackDefinitionByName(ctx, defName)
		if err != nil {
			fmt.Println("Error looking up stack definition:", err)
			os.Exit(1)
		}
		if def == nil {
			fmt.Printf("Stack definition %q not found.\n", defName)
			os.Exit(1)
		}

		// Resolve template names to IDs for each component
		type createdSpace struct {
			key   string
			id    string
			space *apiclient.StackDefSpace
		}
		spaces := make([]createdSpace, 0, len(def.Spaces))

		if exists, err := client.StackExists(ctx, stackName); err != nil {
			fmt.Println("Error checking for existing stack:", err)
			os.Exit(1)
		} else if exists {
			fmt.Printf("Stack %q already exists. Use a different stack name or delete the existing stack first.\n", stackName)
			os.Exit(1)
		}

		for i := range def.Spaces {
			comp := &def.Spaces[i]
			spaceName := prefix + "-" + comp.Name

			templateId := comp.TemplateId

			customFields := make([]apiclient.CustomFieldValue, 0, len(comp.CustomFields))
			for _, cf := range comp.CustomFields {
				customFields = append(customFields, apiclient.CustomFieldValue{
					Name:  cf.Name,
					Value: cf.Value,
				})
			}

			spaceId, _, err := client.CreateSpace(ctx, &apiclient.SpaceRequest{
				Name:         spaceName,
				TemplateId:   templateId,
				Stack:        stackName,
				StackPrefix:  prefix,
				Description:  comp.Description,
				Shell:        comp.Shell,
				CustomFields: customFields,
			})
			if err != nil {
				fmt.Printf("Error creating space %q: %v\n", spaceName, err)

				for _, s := range spaces {
					client.DeleteSpace(ctx, s.id)
				}
				os.Exit(1)
			}

			spaces = append(spaces, createdSpace{key: comp.Name, id: spaceId, space: comp})
			fmt.Printf("  Created space %q (%s)\n", spaceName, spaceId)
		}

		keyToID := make(map[string]string)
		for _, s := range spaces {
			keyToID[s.key] = s.id
		}

		for _, s := range spaces {
			if len(s.space.DependsOn) == 0 {
				continue
			}
			depIDs := make([]string, 0, len(s.space.DependsOn))
			for _, depKey := range s.space.DependsOn {
				if id, ok := keyToID[depKey]; ok {
					depIDs = append(depIDs, id)
				} else {
					fmt.Printf("  Warning: dependency %q not found for space %q\n", depKey, s.key)
				}
			}
			if len(depIDs) > 0 {
				spaceName := prefix + "-" + s.key
				_, err := client.UpdateSpace(ctx, s.id, &apiclient.SpaceRequest{
					Name:      spaceName,
					DependsOn: depIDs,
					Stack:     stackName,
				})
				if err != nil {
					fmt.Printf("  Warning: failed to set dependencies for %q: %v\n", spaceName, err)
				}
			}
		}

		for _, s := range spaces {
			if len(s.space.PortForwards) == 0 {
				continue
			}
			forwards := make([]apiclient.PortForwardRequest, 0, len(s.space.PortForwards))
			for _, pf := range s.space.PortForwards {
				targetID, ok := keyToID[pf.ToSpace]
				if !ok {
					fmt.Printf("  Warning: port forward target %q not found for space %q\n", pf.ToSpace, s.key)
					continue
				}
				forwards = append(forwards, apiclient.PortForwardRequest{
					LocalPort:  pf.LocalPort,
					Space:      targetID,
					RemotePort: pf.RemotePort,
					Persistent: true,
				})
			}
			if len(forwards) > 0 {
				_, _, err := client.ApplyPorts(ctx, s.id, &apiclient.PortApplyRequest{Forwards: forwards})
				if err != nil {
					fmt.Printf("  Warning: failed to apply port forwards for space %q: %v\n", s.key, err)
				}
			}
		}

		fmt.Printf("\nStack %q created from definition %q with %d space(s).\n", stackName, defName, len(spaces))
		fmt.Printf("Run 'knot stack start %s' to start all spaces.\n", stackName)
		return nil
	},
}
View Source
var CreateDefCmd = &cli.Command{
	Name:        "create-def",
	Usage:       "Create a new stack definition",
	Description: "Create a new stack definition from a TOML or JSON file.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "file",
			Usage:    "Path to the TOML or JSON definition file",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		req, err := loadStackDef(ctx, cmd.GetStringArg("file"), client)
		if err != nil {
			fmt.Println("Error reading definition:", err)
			os.Exit(1)
		}

		existing, err := client.GetStackDefinitionByName(ctx, req.Name)
		if err != nil {
			fmt.Println("Error checking for existing definition:", err)
			os.Exit(1)
		}
		if existing != nil {
			fmt.Printf("Stack definition %q already exists. Use 'apply' to update it.\n", req.Name)
			os.Exit(1)
		}

		_, _, err = client.CreateStackDefinition(ctx, req)
		if err != nil {
			fmt.Println("Error creating stack definition:", err)
			os.Exit(1)
		}

		fmt.Printf("Stack definition %q created.\n", req.Name)
		return nil
	},
}
View Source
var DeleteCmd = &cli.Command{
	Name:        "delete",
	Usage:       "Delete a stack and all its spaces",
	Description: "Delete all spaces belonging to the named stack.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "name",
			Usage:    "Name of the stack to delete",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "yes",
			Aliases: []string{"y"},
			Usage:   "Skip confirmation prompt",
		},
	},
	Run: func(ctx context.Context, cmd *cli.Command) error {
		name := cmd.GetStringArg("name")

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		user, err := client.WhoAmI(ctx)
		if err != nil {
			fmt.Println("Error getting user:", err)
			os.Exit(1)
		}

		spaces, _, err := client.GetSpaces(ctx, user.Id, false)
		if err != nil {
			fmt.Println("Error getting spaces:", err)
			os.Exit(1)
		}

		// Collect spaces belonging to this stack
		type stackSpace struct {
			id   string
			name string
		}
		var stackSpaces []stackSpace
		for _, s := range spaces.Spaces {
			if s.Stack == name {
				stackSpaces = append(stackSpaces, stackSpace{id: s.Id, name: s.Name})
			}
		}

		if len(stackSpaces) == 0 {
			fmt.Printf("No spaces found for stack %q.\n", name)
			return nil
		}

		if !cmd.GetBool("yes") {
			fmt.Printf("Delete stack %q and its %d space(s)?\n", name, len(stackSpaces))
			for _, s := range stackSpaces {
				fmt.Printf("  - %s\n", s.name)
			}
			fmt.Print("\n[y/N] ")
			reader := bufio.NewReader(os.Stdin)
			answer, _ := reader.ReadString('\n')
			if strings.ToLower(strings.TrimSpace(answer)) != "y" {
				fmt.Println("Aborted.")
				return nil
			}
		}

		_, err = client.DeleteStack(ctx, name)
		if err != nil {
			fmt.Printf("Error deleting stack %q: %v\n", name, err)
			os.Exit(1)
		}

		fmt.Printf("Stack %q deleting.\n", name)
		return nil
	},
}
View Source
var DeleteDefCmd = &cli.Command{
	Name:        "delete-def",
	Usage:       "Delete a stack definition",
	Description: "Delete a stack definition by name.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "name",
			Usage:    "Name of the stack definition to delete",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		name := cmd.GetStringArg("name")

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		def, err := client.GetStackDefinitionByName(ctx, name)
		if err != nil {
			fmt.Println("Error looking up stack definition:", err)
			os.Exit(1)
		}
		if def == nil {
			fmt.Printf("Stack definition %q not found.\n", name)
			os.Exit(1)
		}

		_, err = client.DeleteStackDefinition(ctx, def.Id)
		if err != nil {
			fmt.Println("Error deleting stack definition:", err)
			os.Exit(1)
		}

		fmt.Printf("Stack definition %q deleted.\n", name)
		return nil
	},
}
View Source
var DisableCmd = &cli.Command{
	Name:        "disable-def",
	Usage:       "Disable a stack definition",
	Description: "Disable a stack definition so it cannot be used to create stacks.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "name",
			Usage:    "Name of the stack definition to disable",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		name := cmd.GetStringArg("name")

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		def, err := client.GetStackDefinitionByName(ctx, name)
		if err != nil {
			fmt.Println("Error looking up stack definition:", err)
			os.Exit(1)
		}
		if def == nil {
			fmt.Printf("Stack definition %q not found.\n", name)
			os.Exit(1)
		}

		if !def.Active {
			fmt.Printf("Stack definition %q is already disabled.\n", name)
			return nil
		}

		_, err = client.UpdateStackDefinition(ctx, def.Id, &apiclient.StackDefinitionRequest{
			Name:        def.Name,
			Description: def.Description,
			Active:      false,
			Scope:       def.Scope,
			Groups:      def.Groups,
			Zones:       def.Zones,
			Spaces:      def.Spaces,
		})
		if err != nil {
			fmt.Println("Error disabling stack definition:", err)
			os.Exit(1)
		}

		fmt.Printf("Stack definition %q disabled.\n", name)
		return nil
	},
}
View Source
var EnableCmd = &cli.Command{
	Name:        "enable-def",
	Usage:       "Enable a stack definition",
	Description: "Enable a stack definition so it can be used to create stacks.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "name",
			Usage:    "Name of the stack definition to enable",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		name := cmd.GetStringArg("name")

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		def, err := client.GetStackDefinitionByName(ctx, name)
		if err != nil {
			fmt.Println("Error looking up stack definition:", err)
			os.Exit(1)
		}
		if def == nil {
			fmt.Printf("Stack definition %q not found.\n", name)
			os.Exit(1)
		}

		if def.Active {
			fmt.Printf("Stack definition %q is already enabled.\n", name)
			return nil
		}

		_, err = client.UpdateStackDefinition(ctx, def.Id, &apiclient.StackDefinitionRequest{
			Name:        def.Name,
			Description: def.Description,
			Active:      true,
			Scope:       def.Scope,
			Groups:      def.Groups,
			Zones:       def.Zones,
			Spaces:      def.Spaces,
		})
		if err != nil {
			fmt.Println("Error enabling stack definition:", err)
			os.Exit(1)
		}

		fmt.Printf("Stack definition %q enabled.\n", name)
		return nil
	},
}
View Source
var ListCmd = &cli.Command{
	Name:        "list",
	Usage:       "List stacks and their status",
	Description: "Lists all stacks for the logged in user and the status and health of their spaces.",
	MaxArgs:     cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		user, err := client.WhoAmI(context.Background())
		if err != nil {
			fmt.Println("Error getting user:", err)
			os.Exit(1)
		}

		spaces, _, err := client.GetSpaces(context.Background(), user.Id, false)
		if err != nil {
			fmt.Println("Error getting spaces:", err)
			os.Exit(1)
		}

		order := []string{}
		stacks := map[string][][]string{}
		for _, space := range spaces.Spaces {
			if space.Stack == "" {
				continue
			}
			if _, seen := stacks[space.Stack]; !seen {
				order = append(order, space.Stack)
				stacks[space.Stack] = [][]string{}
			}

			stacks[space.Stack] = append(stacks[space.Stack], []string{
				fmt.Sprintf("%s (%s)", space.Name, stackSpaceStatus(space)),
				stackSpaceHealth(space),
			})
		}

		if len(order) == 0 {
			fmt.Println("No stacks found.")
			return nil
		}

		data := [][]string{{"Stack", "Spaces", "Health"}}
		for _, name := range order {
			first := true
			for _, entry := range stacks[name] {
				if first {
					data = append(data, []string{name, entry[0], entry[1]})
					first = false
				} else {
					data = append(data, []string{"", entry[0], entry[1]})
				}
			}
		}

		util.PrintTable(data)
		return nil
	},
}
View Source
var ListDefsCmd = &cli.Command{
	Name:        "list-defs",
	Usage:       "List stack definitions",
	Description: "List all stack definitions with details.",
	MaxArgs:     cli.NoArgs,
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:  "details",
			Usage: "Show space details for each definition",
		},
	},
	Run: func(ctx context.Context, cmd *cli.Command) error {
		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		list, _, err := client.GetStackDefinitions(ctx)
		if err != nil {
			fmt.Println("Error listing stack definitions:", err)
			os.Exit(1)
		}

		if list.Count == 0 {
			fmt.Println("No stack definitions found.")
			return nil
		}

		details := cmd.GetBool("details")

		if !details {
			data := [][]string{{"Name", "Scope", "Zones", "Spaces", "Active", "Description"}}
			for _, d := range list.Definitions {
				zones := "all"
				if len(d.Zones) > 0 {
					zones = strings.Join(d.Zones, ",")
				}
				active := "yes"
				if !d.Active {
					active = "no"
				}
				data = append(data, []string{
					d.Name,
					d.Scope,
					zones,
					fmt.Sprintf("%d", len(d.Spaces)),
					active,
					d.Description,
				})
			}
			util.PrintTable(data)
			return nil
		}

		for _, d := range list.Definitions {
			zones := "all zones"
			if len(d.Zones) > 0 {
				zones = strings.Join(d.Zones, ", ")
			}
			fmt.Printf("%s (%s, %s)", d.Name, d.Scope, zones)
			if d.Description != "" {
				fmt.Printf(" — %s", d.Description)
			}
			fmt.Println()
			for _, s := range d.Spaces {
				depends := "(none)"
				if len(s.DependsOn) > 0 {
					depends = strings.Join(s.DependsOn, ", ")
				}
				forwards := "(none)"
				if len(s.PortForwards) > 0 {
					parts := make([]string, 0, len(s.PortForwards))
					for _, pf := range s.PortForwards {
						parts = append(parts, fmt.Sprintf("%s:%d → %s:%d", s.Name, pf.LocalPort, pf.ToSpace, pf.RemotePort))
					}
					forwards = strings.Join(parts, ", ")
				}
				fmt.Printf("  %-6s %-20s depends: [%s]  forwards: %s\n", s.Name, s.TemplateId, depends, forwards)
			}
		}

		return nil
	},
}
View Source
var RestartCmd = &cli.Command{
	Name:        "restart",
	Usage:       "Restart a stack",
	Description: "Restart all spaces in the named stack.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "stack",
			Usage:    "The name of the stack to restart",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		stackName := cmd.GetStringArg("stack")
		fmt.Println("Restarting stack: ", stackName)

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			return fmt.Errorf("Failed to create API client: %w", err)
		}

		_, err = client.RestartStack(context.Background(), stackName)
		if err != nil {
			return fmt.Errorf("Error restarting stack: %w", err)
		}

		fmt.Println("Stack restarted: ", stackName)
		return nil
	},
}
View Source
var StackCmd = &cli.Command{
	Name:        "stack",
	Usage:       "Manage stacks",
	Description: "Manage your stacks from the command line.",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "server",
			Aliases: []string{"s"},
			Usage:   "The address of the remote server to manage stacks on.",
			EnvVars: []string{config.CONFIG_ENV_PREFIX + "_SERVER"},
			Global:  true,
		},
		&cli.StringFlag{
			Name:    "token",
			Aliases: []string{"t"},
			Usage:   "The token to use for authentication.",
			EnvVars: []string{config.CONFIG_ENV_PREFIX + "_TOKEN"},
			Global:  true,
		},
		&cli.BoolFlag{
			Name:         "tls-skip-verify",
			Usage:        "Skip TLS verification when talking to server.",
			ConfigPath:   []string{"tls.skip_verify"},
			EnvVars:      []string{config.CONFIG_ENV_PREFIX + "_TLS_SKIP_VERIFY"},
			DefaultValue: true,
			Global:       true,
		},
		&cli.StringFlag{
			Name:         "alias",
			Aliases:      []string{"a"},
			Usage:        "The server alias to use.",
			DefaultValue: "default",
			Global:       true,
		},
	},
	Commands: []*cli.Command{
		ValidateCmd,
		CreateDefCmd,
		ApplyCmd,
		DeleteDefCmd,
		EnableCmd,
		DisableCmd,
		ListDefsCmd,
		ListCmd,
		CreateCmd,
		StartCmd,
		StopCmd,
		RestartCmd,
		DeleteCmd,
	},
}
View Source
var StartCmd = &cli.Command{
	Name:        "start",
	Usage:       "Start a stack",
	Description: "Start all spaces in the named stack.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "stack",
			Usage:    "The name of the stack to start",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		stackName := cmd.GetStringArg("stack")
		fmt.Println("Starting stack: ", stackName)

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			return fmt.Errorf("Failed to create API client: %w", err)
		}

		_, err = client.StartStack(context.Background(), stackName)
		if err != nil {
			return fmt.Errorf("Error starting stack: %w", err)
		}

		fmt.Println("Stack started: ", stackName)
		return nil
	},
}
View Source
var StopCmd = &cli.Command{
	Name:        "stop",
	Usage:       "Stop a stack",
	Description: "Stop all spaces in the named stack.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "stack",
			Usage:    "The name of the stack to stop",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		stackName := cmd.GetStringArg("stack")
		fmt.Println("Stopping stack: ", stackName)

		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			return fmt.Errorf("Failed to create API client: %w", err)
		}

		_, err = client.StopStack(context.Background(), stackName)
		if err != nil {
			return fmt.Errorf("Error stopping stack: %w", err)
		}

		fmt.Println("Stack stopped: ", stackName)
		return nil
	},
}
View Source
var ValidateCmd = &cli.Command{
	Name:        "validate",
	Usage:       "Validate a stack definition file",
	Description: "Validate a stack definition TOML or JSON file without creating it.",
	Arguments: []cli.Argument{
		&cli.StringArg{
			Name:     "file",
			Usage:    "Path to the TOML or JSON definition file",
			Required: true,
		},
	},
	MaxArgs: cli.NoArgs,
	Run: func(ctx context.Context, cmd *cli.Command) error {
		client, err := cmdutil.GetClient(cmd)
		if err != nil {
			fmt.Println("Failed to create API client:", err)
			os.Exit(1)
		}

		req, err := loadStackDef(ctx, cmd.GetStringArg("file"), client)
		if err != nil {
			fmt.Println("Error reading definition:", err)
			os.Exit(1)
		}

		result, _, err := client.ValidateStackDefinition(ctx, req)
		if err != nil {
			fmt.Println("Error validating definition:", err)
			os.Exit(1)
		}

		if result.Valid {
			fmt.Println("Stack definition is valid.")
			return nil
		}

		fmt.Printf("Stack definition has %d error(s):\n", len(result.Errors))
		for _, e := range result.Errors {
			if e.Space != "" {
				fmt.Printf("  [%s] %s: %s\n", e.Space, e.Field, e.Message)
			} else {
				fmt.Printf("  %s: %s\n", e.Field, e.Message)
			}
		}
		os.Exit(1)
		return nil
	},
}

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

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