fix: improve talos bootstrap flow
This commit is contained in:
parent
b6dce951ed
commit
ef654073d6
|
|
@ -15,7 +15,7 @@ dlv version
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- `cicd-tool repo init --org <org> --flux-repo <repo>` creates the manifests and Flux repos
|
- `cicd-tool repo init --org <org> --flux-repo <repo>` creates the manifests and Flux repos
|
||||||
- `cicd-tool bootstrap` runs a shorter Forgejo-first wizard, discovers Proxmox nodes/storage/networks, shows the latest Talos version, writes `terraform.tfvars`, stages Talos images on Proxmox, and can execute Terraform, Talos bootstrap, and Flux bootstrap
|
- `cicd-tool bootstrap` runs a shorter Forgejo-first wizard, asks for a Forgejo PAT, asks where local repos should be cloned, discovers Proxmox nodes/storage/networks, retries without losing entered answers when discovery fails, shows the latest Talos version, derives the standardized Talos factory URL, schematic, and required extensions automatically from the chosen version, writes `terraform.tfvars`, stages Talos images on Proxmox, and can execute Terraform, Talos bootstrap, and Flux bootstrap
|
||||||
- `cicd-tool bootstrap --config maidn-bootstrap.yaml` skips the wizard and uses the saved config
|
- `cicd-tool bootstrap --config maidn-bootstrap.yaml` skips the wizard and uses the saved config
|
||||||
|
|
||||||
## Forgejo setup
|
## Forgejo setup
|
||||||
|
|
@ -26,3 +26,6 @@ For `https://git.pingu.pw` you need:
|
||||||
- an owner target (`Maidn` org by default, or your own user/org)
|
- an owner target (`Maidn` org by default, or your own user/org)
|
||||||
- git/ssh access from the machine running the CLI if you want SSH later
|
- git/ssh access from the machine running the CLI if you want SSH later
|
||||||
- Flux bootstrap credentials for the repo URL that gets created
|
- Flux bootstrap credentials for the repo URL that gets created
|
||||||
|
|
||||||
|
root@pam!maidn-test-key
|
||||||
|
2ce7bff1-ac98-4a45-8db4-186d49ae4159
|
||||||
|
|
@ -32,7 +32,7 @@ func runBootstrap(cmd *cobra.Command, args []string) {
|
||||||
if bootstrapConfigPath != "" {
|
if bootstrapConfigPath != "" {
|
||||||
cfg, err = config.Load(bootstrapConfigPath)
|
cfg, err = config.Load(bootstrapConfigPath)
|
||||||
} else {
|
} else {
|
||||||
cfg, err = ui.RunBootstrapWizard()
|
cfg, err = ui.RunBootstrapWizard(config.Config{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = config.Save(bootstrapOutputPath, cfg)
|
err = config.Save(bootstrapOutputPath, cfg)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,14 @@ func (r Runner) Run() error {
|
||||||
if err := os.MkdirAll(workspace, 0755); err != nil {
|
if err := os.MkdirAll(workspace, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := config.Save(filepath.Join(workspace, "maidn-bootstrap.resolved.yaml"), r.Config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(r.Config.Git.CloneParent, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
manifestsURL := forgejo.CloneURL(r.Config.Git.BaseURL, r.Config.Git.Owner, r.Config.Flux.ManifestsRepo)
|
manifestsURL := forgejo.CloneURL(r.Config.Git.BaseURL, r.Config.Git.Owner, r.Config.Flux.ManifestsRepo)
|
||||||
fluxConfig := ghrepo.BuildFluxConfig(strings.ToLower(r.Config.Git.Owner), manifestsURL, r.Config.Flux.ManifestsRepo)
|
fluxConfig := ghrepo.BuildFluxConfig(strings.ToLower(r.Config.Git.Owner), manifestsURL, r.Config.Flux.ManifestsRepo)
|
||||||
|
|
@ -88,7 +96,7 @@ func renderTerraformTFVars(cfg config.Config) string {
|
||||||
builder.WriteString(fmt.Sprintf("proxmox_pool = %q\n", cfg.Talos.Proxmox.Pool))
|
builder.WriteString(fmt.Sprintf("proxmox_pool = %q\n", cfg.Talos.Proxmox.Pool))
|
||||||
builder.WriteString(fmt.Sprintf("cluster_name = %q\n", cfg.Talos.Cluster.Name))
|
builder.WriteString(fmt.Sprintf("cluster_name = %q\n", cfg.Talos.Cluster.Name))
|
||||||
builder.WriteString(fmt.Sprintf("cluster_domain = %q\n", cfg.Talos.Cluster.Domain))
|
builder.WriteString(fmt.Sprintf("cluster_domain = %q\n", cfg.Talos.Cluster.Domain))
|
||||||
builder.WriteString(fmt.Sprintf("talos_factory_schematic_id = %q\n", cfg.Talos.Image.FactorySchematicID))
|
builder.WriteString(fmt.Sprintf("talos_factory_schematic_id = %q\n", cfg.Talos.Image.SchematicID))
|
||||||
builder.WriteString(fmt.Sprintf("talos_version = %q\n", cfg.Talos.Image.TalosVersion))
|
builder.WriteString(fmt.Sprintf("talos_version = %q\n", cfg.Talos.Image.TalosVersion))
|
||||||
builder.WriteString("cni_name = \"flannel\"\n")
|
builder.WriteString("cni_name = \"flannel\"\n")
|
||||||
builder.WriteString(fmt.Sprintf("talos_iso_file = %q\n", fmt.Sprintf("%s:iso/talos-%s.iso", cfg.Talos.Image.Storage, strings.TrimPrefix(cfg.Talos.Image.TalosVersion, "v"))))
|
builder.WriteString(fmt.Sprintf("talos_iso_file = %q\n", fmt.Sprintf("%s:iso/talos-%s.iso", cfg.Talos.Image.Storage, strings.TrimPrefix(cfg.Talos.Image.TalosVersion, "v"))))
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,8 @@ func Validate(cfg Config) error {
|
||||||
if cfg.Talos.Cluster.Name == "" || cfg.Talos.Cluster.Domain == "" {
|
if cfg.Talos.Cluster.Name == "" || cfg.Talos.Cluster.Domain == "" {
|
||||||
return errors.New("talos cluster name and domain are required")
|
return errors.New("talos cluster name and domain are required")
|
||||||
}
|
}
|
||||||
if cfg.Talos.Image.FactorySchematicID == "" || cfg.Talos.Image.TalosVersion == "" {
|
if cfg.Talos.Image.TalosVersion == "" || cfg.Talos.Image.SchematicID == "" {
|
||||||
return errors.New("talos image pins are required")
|
return errors.New("talos version and schematicId are required")
|
||||||
}
|
}
|
||||||
if cfg.Talos.Image.UpdateMode != "manual" && cfg.Talos.Image.UpdateMode != "download" {
|
if cfg.Talos.Image.UpdateMode != "manual" && cfg.Talos.Image.UpdateMode != "download" {
|
||||||
return errors.New("talos image updateMode must be manual or download")
|
return errors.New("talos image updateMode must be manual or download")
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,11 @@ type TalosClusterConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type TalosImageConfig struct {
|
type TalosImageConfig struct {
|
||||||
FactorySchematicID string `yaml:"factorySchematicId"`
|
TalosVersion string `yaml:"talosVersion"`
|
||||||
TalosVersion string `yaml:"talosVersion"`
|
SchematicID string `yaml:"schematicId"`
|
||||||
UpdateMode string `yaml:"updateMode"`
|
UpdateMode string `yaml:"updateMode"`
|
||||||
Storage string `yaml:"storage"`
|
Storage string `yaml:"storage"`
|
||||||
Architecture string `yaml:"architecture"`
|
Architecture string `yaml:"architecture"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TalosNode struct {
|
type TalosNode struct {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type createRepoRequest struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Private bool `json:"private"`
|
Private bool `json:"private"`
|
||||||
AutoInit bool `json:"auto_init"`
|
AutoInit bool `json:"auto_init"`
|
||||||
|
DefaultBranch string `json:"default_branch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepoManager(baseURL, token, owner, username, manifestsRepo, fluxRepo string) *RepoManager {
|
func NewRepoManager(baseURL, token, owner, username, manifestsRepo, fluxRepo string) *RepoManager {
|
||||||
|
|
@ -59,7 +60,7 @@ func (rm *RepoManager) ensureRepo(name, description string, createStructure func
|
||||||
if rm.Owner == rm.Username {
|
if rm.Owner == rm.Username {
|
||||||
createURL = fmt.Sprintf("%s/api/v1/user/repos", rm.BaseURL)
|
createURL = fmt.Sprintf("%s/api/v1/user/repos", rm.BaseURL)
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(createRepoRequest{Name: name, Description: description, Private: true, AutoInit: true})
|
body, _ := json.Marshal(createRepoRequest{Name: name, Description: description, Private: true, AutoInit: false, DefaultBranch: "main"})
|
||||||
if apiErr := rm.apiRequest(http.MethodPost, createURL, body); apiErr != nil {
|
if apiErr := rm.apiRequest(http.MethodPost, createURL, body); apiErr != nil {
|
||||||
return apiErr
|
return apiErr
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +77,9 @@ func (rm *RepoManager) setupRepository(repoURL, repoName string, createStructure
|
||||||
if err := utils.RunCommand("git", "clone", repoURL, tempDir); err != nil {
|
if err := utils.RunCommand("git", "clone", repoURL, tempDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := utils.RunCommandInDir(tempDir, "git", "checkout", "-B", "main"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := createStructure(tempDir); err != nil {
|
if err := createStructure(tempDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
@ -12,10 +13,27 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
tokenID string
|
tokenID string
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIError struct {
|
||||||
|
StatusCode int
|
||||||
|
Message string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e APIError) Error() string {
|
||||||
|
if e.Message == "" {
|
||||||
|
return fmt.Sprintf("proxmox returned %d for %s", e.StatusCode, e.Path)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("proxmox returned %d for %s: %s", e.StatusCode, e.Path, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e APIError) IsPermissionDenied() bool {
|
||||||
|
return e.StatusCode == http.StatusForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
type clusterNodesResponse struct {
|
type clusterNodesResponse struct {
|
||||||
|
|
@ -57,19 +75,20 @@ type storageResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeInfo struct {
|
type NodeInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Networks []string
|
Networks []string
|
||||||
Storages []StorageInfo
|
Storages []StorageInfo
|
||||||
FreeMemoryMB int
|
FreeMemoryMB int
|
||||||
TotalMemoryMB int
|
TotalMemoryMB int
|
||||||
FreeDiskGB int
|
FreeDiskGB int
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageInfo struct {
|
type StorageInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
FreeGB int
|
FreeGB int
|
||||||
SupportsISO bool
|
SupportsISO bool
|
||||||
|
SupportsDisk bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(baseURL, tokenID, tokenSecret string, insecure bool) *Client {
|
func New(baseURL, tokenID, tokenSecret string, insecure bool) *Client {
|
||||||
|
|
@ -97,10 +116,17 @@ func (c *Client) Discover() ([]NodeInfo, error) {
|
||||||
}
|
}
|
||||||
info, err := c.describeNode(node.Node)
|
info, err := c.describeNode(node.Node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
var apiErr APIError
|
||||||
|
if ok := AsAPIError(err, &apiErr); ok && apiErr.IsPermissionDenied() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result = append(result, info)
|
result = append(result, info)
|
||||||
}
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, fmt.Errorf("no Proxmox nodes could be inspected; token likely needs Sys.Audit on target nodes")
|
||||||
|
}
|
||||||
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
|
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
@ -131,10 +157,11 @@ func (c *Client) describeNode(node string) (NodeInfo, error) {
|
||||||
}
|
}
|
||||||
for _, storage := range storages.Data {
|
for _, storage := range storages.Data {
|
||||||
info.Storages = append(info.Storages, StorageInfo{
|
info.Storages = append(info.Storages, StorageInfo{
|
||||||
Name: storage.Storage,
|
Name: storage.Storage,
|
||||||
Type: storage.Type,
|
Type: storage.Type,
|
||||||
FreeGB: int(storage.Avail / 1024 / 1024 / 1024),
|
FreeGB: int(storage.Avail / 1024 / 1024 / 1024),
|
||||||
SupportsISO: storage.Type == "dir" || storage.Type == "nfs",
|
SupportsISO: storage.Type == "dir" || storage.Type == "nfs",
|
||||||
|
SupportsDisk: storage.Type == "dir" || storage.Type == "nfs" || storage.Type == "lvm" || storage.Type == "lvmthin" || storage.Type == "zfspool" || storage.Type == "btrfs",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return info, nil
|
return info, nil
|
||||||
|
|
@ -152,11 +179,20 @@ func (c *Client) getJSON(path string, target any) error {
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
if response.StatusCode >= 300 {
|
if response.StatusCode >= 300 {
|
||||||
return fmt.Errorf("proxmox returned %s", response.Status)
|
body, _ := io.ReadAll(response.Body)
|
||||||
|
return APIError{StatusCode: response.StatusCode, Message: strings.TrimSpace(string(body)), Path: path}
|
||||||
}
|
}
|
||||||
return json.NewDecoder(response.Body).Decode(target)
|
return json.NewDecoder(response.Body).Decode(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AsAPIError(err error, target *APIError) bool {
|
||||||
|
apiErr, ok := err.(APIError)
|
||||||
|
if ok {
|
||||||
|
*target = apiErr
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
func BuildAPIURL(host string) string {
|
func BuildAPIURL(host string) string {
|
||||||
if strings.HasPrefix(host, "http") {
|
if strings.HasPrefix(host, "http") {
|
||||||
return strings.TrimRight(host, "/")
|
return strings.TrimRight(host, "/")
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,42 @@
|
||||||
package talos
|
package talos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPlatform = "nocloud"
|
||||||
|
defaultTarget = "cloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultExtensions = []string{
|
||||||
|
"siderolabs/iscsi-tools",
|
||||||
|
"siderolabs/nfs-utils",
|
||||||
|
"siderolabs/qemu-guest-agent",
|
||||||
|
"siderolabs/util-linux-tools",
|
||||||
|
}
|
||||||
|
|
||||||
type releaseResponse struct {
|
type releaseResponse struct {
|
||||||
TagName string `json:"tag_name"`
|
TagName string `json:"tag_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type schematicRequest struct {
|
||||||
|
Customization struct {
|
||||||
|
SystemExtensions struct {
|
||||||
|
OfficialExtensions []string `json:"officialExtensions"`
|
||||||
|
} `json:"systemExtensions"`
|
||||||
|
} `json:"customization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type schematicResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
func LatestVersion() string {
|
func LatestVersion() string {
|
||||||
client := http.Client{Timeout: 10 * time.Second}
|
client := http.Client{Timeout: 10 * time.Second}
|
||||||
request, err := http.NewRequest(http.MethodGet, "https://api.github.com/repos/siderolabs/talos/releases/latest", nil)
|
request, err := http.NewRequest(http.MethodGet, "https://api.github.com/repos/siderolabs/talos/releases/latest", nil)
|
||||||
|
|
@ -33,6 +59,54 @@ func LatestVersion() string {
|
||||||
return release.TagName
|
return release.TagName
|
||||||
}
|
}
|
||||||
|
|
||||||
func FactoryHint(version string) string {
|
func NormalizeVersion(version string) string {
|
||||||
return fmt.Sprintf("https://factory.talos.dev/?version=%s", version)
|
if strings.HasPrefix(version, "v") {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
return "v" + version
|
||||||
|
}
|
||||||
|
|
||||||
|
func FactoryURL(version, arch string) string {
|
||||||
|
version = NormalizeVersion(version)
|
||||||
|
return fmt.Sprintf("https://factory.talos.dev/?arch=%s&extensions=-&extensions=%s&extensions=%s&extensions=%s&extensions=%s&platform=%s&target=%s&version=%s",
|
||||||
|
arch,
|
||||||
|
"siderolabs%2Fiscsi-tools",
|
||||||
|
"siderolabs%2Fnfs-utils",
|
||||||
|
"siderolabs%2Fqemu-guest-agent",
|
||||||
|
"siderolabs%2Futil-linux-tools",
|
||||||
|
defaultPlatform,
|
||||||
|
defaultTarget,
|
||||||
|
strings.TrimPrefix(version, "v"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveSchematicID() (string, error) {
|
||||||
|
requestBody := schematicRequest{}
|
||||||
|
requestBody.Customization.SystemExtensions.OfficialExtensions = defaultExtensions
|
||||||
|
payload, err := json.Marshal(requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
client := http.Client{Timeout: 15 * time.Second}
|
||||||
|
request, err := http.NewRequest(http.MethodPost, "https://factory.talos.dev/schematics", bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode >= 300 {
|
||||||
|
return "", fmt.Errorf("talos factory returned %s", response.Status)
|
||||||
|
}
|
||||||
|
var schematic schematicResponse
|
||||||
|
if err := json.NewDecoder(response.Body).Decode(&schematic); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if schematic.ID == "" {
|
||||||
|
return "", fmt.Errorf("talos factory returned empty schematic id")
|
||||||
|
}
|
||||||
|
return schematic.ID, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,7 @@ func GetUserInputForGithubAuth() (username, token string, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
|
func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
|
||||||
fmt.Printf("%s: ", prompt)
|
token := promptSecret(reader, prompt, "")
|
||||||
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
|
|
||||||
fmt.Println()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
token := string(tokenBytes)
|
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return "", fmt.Errorf("token cannot be empty")
|
return "", fmt.Errorf("token cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
@ -118,3 +111,21 @@ func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
|
||||||
|
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptSecret(reader *bufio.Reader, prompt, fallback string) string {
|
||||||
|
if fallback != "" {
|
||||||
|
fmt.Printf("%s [saved]: ", prompt)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: ", prompt)
|
||||||
|
}
|
||||||
|
secretBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
secret := strings.TrimSpace(string(secretBytes))
|
||||||
|
if secret == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,64 +12,75 @@ import (
|
||||||
"github.com/Pingu-Studio/MaidnCLI/internal/talos"
|
"github.com/Pingu-Studio/MaidnCLI/internal/talos"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunBootstrapWizard() (config.Config, error) {
|
func RunBootstrapWizard(initial config.Config) (config.Config, error) {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
cfg := config.Config{}
|
cfg := initial
|
||||||
cfg.WorkspaceDir = prompt(reader, "Workspace directory", ".\\maidn-workspace")
|
cwd, _ := os.Getwd()
|
||||||
|
if cfg.WorkspaceDir == "" {
|
||||||
|
cfg.WorkspaceDir = cwd
|
||||||
|
}
|
||||||
cfg.Git.Provider = "forgejo"
|
cfg.Git.Provider = "forgejo"
|
||||||
cfg.Git.BaseURL = prompt(reader, "Git server URL", "https://git.pingu.pw")
|
cfg.Git.BaseURL = prompt(reader, "Git server URL", fallback(cfg.Git.BaseURL, "https://git.pingu.pw"))
|
||||||
cfg.Git.Username = prompt(reader, "Git username", "eding")
|
cfg.Git.Username = prompt(reader, "Git username", fallback(cfg.Git.Username, "eding"))
|
||||||
cfg.Git.Owner = prompt(reader, "Repo owner/user", "Maidn")
|
cfg.Git.Owner = prompt(reader, "Repo owner/user", fallback(cfg.Git.Owner, "Maidn"))
|
||||||
cfg.Git.Token = prompt(reader, "Forgejo access token", "")
|
cfg.Git.Token = promptSecret(reader, "Forgejo PAT", cfg.Git.Token)
|
||||||
cfg.Git.CloneParent = prompt(reader, "Clone directory", cfg.WorkspaceDir)
|
cfg.WorkspaceDir = prompt(reader, "Working directory", cfg.WorkspaceDir)
|
||||||
cfg.Flux.RepoName = prompt(reader, "Flux repo name", "test-maidn-cd-cluster")
|
cfg.Git.CloneParent = prompt(reader, "Where should repos be cloned locally", fallback(cfg.Git.CloneParent, cfg.WorkspaceDir))
|
||||||
cfg.Flux.ManifestsRepo = prompt(reader, "Manifests repo name", "cicd-deployment-manifests")
|
cfg.Flux.RepoName = prompt(reader, "Flux repo name", fallback(cfg.Flux.RepoName, "test-maidn-cd-cluster"))
|
||||||
|
cfg.Flux.ManifestsRepo = prompt(reader, "Manifests repo name", fallback(cfg.Flux.ManifestsRepo, "cicd-deployment-manifests"))
|
||||||
cfg.Flux.Branch = "main"
|
cfg.Flux.Branch = "main"
|
||||||
cfg.Flux.ClusterPath = "./clusters/maidn-cd-0"
|
cfg.Flux.ClusterPath = "./clusters/maidn-cd-0"
|
||||||
cfg.Templates.TalosRepoURL = prompt(reader, "Talos template repo URL", "https://git.pingu.pw/Maidn/maidn-talos-proxmox.git")
|
cfg.Templates.TalosRepoURL = prompt(reader, "Talos template repo URL", fallback(cfg.Templates.TalosRepoURL, "https://git.pingu.pw/Maidn/maidn-talos-proxmox.git"))
|
||||||
|
fmt.Printf("Template repos are cloned under: %s\n", cfg.Git.CloneParent)
|
||||||
cfg.Templates.TalosRepoRef = "main"
|
cfg.Templates.TalosRepoRef = "main"
|
||||||
cfg.Talos.RepoDirName = "maidn-talos-proxmox"
|
cfg.Talos.RepoDirName = "maidn-talos-proxmox"
|
||||||
cfg.Talos.TerraformDir = "terraform"
|
cfg.Talos.TerraformDir = "terraform"
|
||||||
cfg.Talos.GeneratedDir = "generated"
|
cfg.Talos.GeneratedDir = "generated"
|
||||||
cfg.Talos.ConfigFileName = "terraform.tfvars"
|
cfg.Talos.ConfigFileName = "terraform.tfvars"
|
||||||
cfg.Talos.AutoRunTerraform = promptBool(reader, "Run terraform automatically", true)
|
cfg.Talos.AutoRunTerraform = promptBool(reader, "Run terraform automatically", cfg.Talos.AutoRunTerraform || !hasInitial(initial))
|
||||||
cfg.Talos.AutoBootstrap = promptBool(reader, "Run talos bootstrap automatically", true)
|
cfg.Talos.AutoBootstrap = promptBool(reader, "Run talos bootstrap automatically", cfg.Talos.AutoBootstrap || !hasInitial(initial))
|
||||||
cfg.Talos.AutoBootstrapFlux = promptBool(reader, "Run flux bootstrap automatically", true)
|
cfg.Talos.AutoBootstrapFlux = promptBool(reader, "Run flux bootstrap automatically", cfg.Talos.AutoBootstrapFlux || !hasInitial(initial))
|
||||||
|
|
||||||
cfg.Talos.Proxmox.APIURL = prompt(reader, "Proxmox API URL", "https://192.168.0.15:8006")
|
cfg.Talos.Proxmox.APIURL = prompt(reader, "Proxmox API URL", fallback(cfg.Talos.Proxmox.APIURL, "https://192.168.0.15:8006"))
|
||||||
cfg.Talos.Proxmox.APITokenID = prompt(reader, "Proxmox API token ID", "root@pam!iac")
|
cfg.Talos.Proxmox.APITokenID = prompt(reader, "Proxmox API token ID", fallback(cfg.Talos.Proxmox.APITokenID, "root@pam!iac"))
|
||||||
cfg.Talos.Proxmox.APITokenSecret = prompt(reader, "Proxmox API token secret", "")
|
cfg.Talos.Proxmox.APITokenSecret = promptSecret(reader, "Proxmox API token secret", cfg.Talos.Proxmox.APITokenSecret)
|
||||||
cfg.Talos.Proxmox.Insecure = true
|
cfg.Talos.Proxmox.Insecure = true
|
||||||
cfg.Talos.Proxmox.Pool = prompt(reader, "Proxmox pool", "")
|
cfg.Talos.Proxmox.Pool = prompt(reader, "Proxmox pool", cfg.Talos.Proxmox.Pool)
|
||||||
|
|
||||||
discovered, err := proxmox.New(cfg.Talos.Proxmox.APIURL, cfg.Talos.Proxmox.APITokenID, cfg.Talos.Proxmox.APITokenSecret, cfg.Talos.Proxmox.Insecure).Discover()
|
discovered, err := discoverProxmox(reader, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
printNodes(discovered)
|
printNodes(discovered)
|
||||||
cfg.Talos.Proxmox.DefaultNode = prompt(reader, "Default Proxmox node", discovered[0].Name)
|
cfg.Talos.Proxmox.DefaultNode = prompt(reader, "Default Proxmox node", fallback(cfg.Talos.Proxmox.DefaultNode, discovered[0].Name))
|
||||||
cfg.Talos.Proxmox.NodeInterfaces = map[string]string{}
|
cfg.Talos.Proxmox.NodeInterfaces = map[string]string{}
|
||||||
for _, node := range discovered {
|
for _, node := range discovered {
|
||||||
fallback := first(node.Networks, "vmbr0")
|
fallbackIface := first(node.Networks, "vmbr0")
|
||||||
cfg.Talos.Proxmox.NodeInterfaces[node.Name] = prompt(reader, fmt.Sprintf("Primary interface for %s", node.Name), fallback)
|
cfg.Talos.Proxmox.NodeInterfaces[node.Name] = prompt(reader, fmt.Sprintf("Primary interface for %s", node.Name), fallbackIface)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Talos.Cluster.Name = prompt(reader, "Talos cluster name", "proxmox-talos-dev02-cluster")
|
cfg.Talos.Cluster.Name = prompt(reader, "Talos cluster name", fallback(cfg.Talos.Cluster.Name, "proxmox-talos-dev02-cluster"))
|
||||||
cfg.Talos.Cluster.Domain = prompt(reader, "Talos cluster domain", "dev02.nid3.com")
|
cfg.Talos.Cluster.Domain = prompt(reader, "Talos cluster domain", fallback(cfg.Talos.Cluster.Domain, "dev02.nid3.com"))
|
||||||
cfg.Flux.ClusterDomain = cfg.Talos.Cluster.Domain
|
cfg.Flux.ClusterDomain = cfg.Talos.Cluster.Domain
|
||||||
cfg.Talos.Cluster.ControlPlaneVIP = prompt(reader, "Control plane VIP", "192.168.45.2")
|
cfg.Talos.Cluster.ControlPlaneVIP = prompt(reader, "Control plane VIP", fallback(cfg.Talos.Cluster.ControlPlaneVIP, "192.168.45.2"))
|
||||||
cfg.Talos.Cluster.DNSServers = promptList(reader, "DNS servers", []string{"192.168.0.222", "1.1.1.1"})
|
cfg.Talos.Cluster.DNSServers = promptList(reader, "DNS servers", fallbackList(cfg.Talos.Cluster.DNSServers, []string{"192.168.0.222", "1.1.1.1"}))
|
||||||
cfg.Talos.Cluster.DiskStorage = prompt(reader, "Primary disk storage", firstStorage(discovered, true, "local-lvm"))
|
cfg.Talos.Cluster.DiskStorage = prompt(reader, "Primary disk storage", fallback(cfg.Talos.Cluster.DiskStorage, firstStorageByCapability(discovered, storageSupportsDisk, "local-lvm")))
|
||||||
cfg.Talos.Cluster.AdditionalStorage = prompt(reader, "Additional disk storage", cfg.Talos.Cluster.DiskStorage)
|
cfg.Talos.Cluster.AdditionalStorage = prompt(reader, "Additional disk storage", fallback(cfg.Talos.Cluster.AdditionalStorage, cfg.Talos.Cluster.DiskStorage))
|
||||||
cfg.Talos.Cluster.CreateVLANInterface = promptBool(reader, "Create VLAN interfaces", true)
|
cfg.Talos.Cluster.CreateVLANInterface = promptBool(reader, "Create VLAN interfaces", cfg.Talos.Cluster.CreateVLANInterface || !hasInitial(initial))
|
||||||
|
|
||||||
latestTalos := talos.LatestVersion()
|
latestTalos := talos.LatestVersion()
|
||||||
cfg.Talos.Image.TalosVersion = prompt(reader, "Talos version", latestTalos)
|
cfg.Talos.Image.TalosVersion = talos.NormalizeVersion(prompt(reader, "Talos version", fallback(cfg.Talos.Image.TalosVersion, latestTalos)))
|
||||||
fmt.Printf("Talos factory: %s\n", talos.FactoryHint(cfg.Talos.Image.TalosVersion))
|
cfg.Talos.Image.Architecture = chooseArchitecture(reader, cfg.Talos.Image.Architecture)
|
||||||
cfg.Talos.Image.FactorySchematicID = prompt(reader, "Talos factory schematic ID", "")
|
fmt.Printf("Talos factory: %s\n", talos.FactoryURL(cfg.Talos.Image.TalosVersion, fallback(cfg.Talos.Image.Architecture, "amd64")))
|
||||||
|
schematicID, err := talos.ResolveSchematicID()
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
cfg.Talos.Image.SchematicID = schematicID
|
||||||
|
fmt.Printf("Talos schematic: %s\n", cfg.Talos.Image.SchematicID)
|
||||||
cfg.Talos.Image.UpdateMode = "download"
|
cfg.Talos.Image.UpdateMode = "download"
|
||||||
cfg.Talos.Image.Storage = prompt(reader, "ISO storage", firstStorage(discovered, false, "local"))
|
cfg.Talos.Image.Storage = prompt(reader, "ISO storage", fallback(cfg.Talos.Image.Storage, firstStorageByCapability(discovered, storageSupportsISO, "local")))
|
||||||
cfg.Talos.Image.Architecture = chooseArchitecture(reader)
|
|
||||||
|
|
||||||
nodePreset := prompt(reader, "Cluster size preset (single/ha/custom)", "single")
|
nodePreset := prompt(reader, "Cluster size preset (single/ha/custom)", "single")
|
||||||
cfg.Talos.Nodes = buildNodes(reader, cfg, discovered, nodePreset)
|
cfg.Talos.Nodes = buildNodes(reader, cfg, discovered, nodePreset)
|
||||||
|
|
@ -85,9 +96,13 @@ func printNodes(nodes []proxmox.NodeInfo) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chooseArchitecture(reader *bufio.Reader) string {
|
func chooseArchitecture(reader *bufio.Reader, current string) string {
|
||||||
fmt.Println("Architecture options: 1) amd64 2) arm64")
|
fmt.Println("Architecture options: 1) amd64 2) arm64")
|
||||||
choice := prompt(reader, "Architecture", "1")
|
defaultChoice := "1"
|
||||||
|
if current == "arm64" {
|
||||||
|
defaultChoice = "2"
|
||||||
|
}
|
||||||
|
choice := prompt(reader, "Architecture", defaultChoice)
|
||||||
if choice == "2" {
|
if choice == "2" {
|
||||||
return "arm64"
|
return "arm64"
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +215,24 @@ func promptList(reader *bufio.Reader, label string, fallback []string) []string
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fallback(value, defaultValue string) string {
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func fallbackList(value, defaultValue []string) []string {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasInitial(cfg config.Config) bool {
|
||||||
|
return cfg.Talos.Proxmox.APIURL != "" || cfg.Git.Token != ""
|
||||||
|
}
|
||||||
|
|
||||||
func first(values []string, fallback string) string {
|
func first(values []string, fallback string) string {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return fallback
|
return fallback
|
||||||
|
|
@ -207,13 +240,10 @@ func first(values []string, fallback string) string {
|
||||||
return values[0]
|
return values[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstStorage(nodes []proxmox.NodeInfo, iso bool, fallback string) string {
|
func firstStorageByCapability(nodes []proxmox.NodeInfo, predicate func(proxmox.StorageInfo) bool, fallback string) string {
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
for _, storage := range node.Storages {
|
for _, storage := range node.Storages {
|
||||||
if iso && storage.SupportsISO {
|
if predicate(storage) {
|
||||||
return storage.Name
|
|
||||||
}
|
|
||||||
if !iso {
|
|
||||||
return storage.Name
|
return storage.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +251,14 @@ func firstStorage(nodes []proxmox.NodeInfo, iso bool, fallback string) string {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func storageSupportsISO(storage proxmox.StorageInfo) bool {
|
||||||
|
return storage.SupportsISO
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageSupportsDisk(storage proxmox.StorageInfo) bool {
|
||||||
|
return storage.SupportsDisk
|
||||||
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
func min(a, b int) int {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
|
|
|
||||||
26
internal/ui/wizard_proxmox.go
Normal file
26
internal/ui/wizard_proxmox.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/proxmox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func discoverProxmox(reader *bufio.Reader, cfg config.Config) ([]proxmox.NodeInfo, error) {
|
||||||
|
for {
|
||||||
|
discovered, err := proxmox.New(cfg.Talos.Proxmox.APIURL, cfg.Talos.Proxmox.APITokenID, cfg.Talos.Proxmox.APITokenSecret, cfg.Talos.Proxmox.Insecure).Discover()
|
||||||
|
if err == nil {
|
||||||
|
return discovered, nil
|
||||||
|
}
|
||||||
|
fmt.Printf("[WARN] Proxmox discovery failed: %v\n", err)
|
||||||
|
fmt.Println("[WARN] Required token perms for discovery: Sys.Audit on nodes/storage/network you want listed.")
|
||||||
|
if !promptBool(reader, "Retry Proxmox discovery", true) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.Talos.Proxmox.APIURL = prompt(reader, "Proxmox API URL", cfg.Talos.Proxmox.APIURL)
|
||||||
|
cfg.Talos.Proxmox.APITokenID = prompt(reader, "Proxmox API token ID", cfg.Talos.Proxmox.APITokenID)
|
||||||
|
cfg.Talos.Proxmox.APITokenSecret = prompt(reader, "Proxmox API token secret", cfg.Talos.Proxmox.APITokenSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue