Skip to content
Merged
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: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: '1.26'
cache: true

- name: Go fmt
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Binary
microshift-installer
microshift-installer.exe
/microshift-installer
/microshift-installer.exe

# Asset / state directories
my-cluster/
Expand Down
270 changes: 270 additions & 0 deletions cmd/microshift-installer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/hchenxa/microshift-installer/pkg/asset"
"github.com/hchenxa/microshift-installer/pkg/config"
"github.com/hchenxa/microshift-installer/pkg/platform"
"github.com/hchenxa/microshift-installer/pkg/state"
"github.com/hchenxa/microshift-installer/pkg/types"
"github.com/spf13/cobra"
)

const stateFileName = "cluster-state.json"

var (
version = "0.1.0"
assetDir string
)

func main() {
rootCmd := &cobra.Command{
Use: "microshift-installer",
Short: "Install and manage MicroShift clusters on AWS and GCP",
Long: `microshift-installer creates and manages MicroShift clusters
on AWS and GCP using the cloud provider's native Go SDKs.

Example:
microshift-installer create cluster --dir=./my-cluster
microshift-installer destroy cluster --dir=./my-cluster
`,
}

rootCmd.PersistentFlags().StringVarP(&assetDir, "dir", "d", "", "Asset directory (default: ~/.microshift-installer/<cluster-name>)")

createCmd := &cobra.Command{
Use: "create",
Short: "Create a MicroShift cluster",
}
createClusterCmd := &cobra.Command{
Use: "cluster",
Short: "Create a new MicroShift cluster",
RunE: runCreateCluster,
}
createCmd.AddCommand(createClusterCmd)

destroyCmd := &cobra.Command{
Use: "destroy",
Short: "Destroy a MicroShift cluster",
}
destroyClusterCmd := &cobra.Command{
Use: "cluster",
Short: "Destroy an existing MicroShift cluster",
RunE: runDestroyCluster,
}
destroyCmd.AddCommand(destroyClusterCmd)

versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("microshift-installer %s\n", version)
},
}

rootCmd.AddCommand(createCmd, destroyCmd, versionCmd)

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

func resolveAssetDir(dir string, name string) (string, error) {
if dir != "" {
return filepath.Abs(dir)
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
clusterName := name
if clusterName == "" {
clusterName = "default"
}
return filepath.Join(home, ".microshift-installer", clusterName), nil
}

func loadOrCreateConfig(store *asset.Store) (*types.InstallConfig, error) {
if store.ConfigExists() {
log.Printf("Found existing install-config.yaml at %s", store.ConfigPath())
cfg, err := config.LoadInstallConfig(store.ConfigPath())
if err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("install config validation failed: %w", err)
}
return cfg, nil
}

log.Print("No install-config.yaml found — starting interactive setup...")
cfg := types.DefaultInstallConfig()
if err := config.Interactive(cfg); err != nil {
return nil, err
}

// Resolve the final asset directory now that we have the cluster name.
resolved, err := resolveAssetDir(assetDir, cfg.Metadata.Name)
if err != nil {
return nil, err
}
assetDir = resolved
store, err = asset.NewStore(assetDir)
if err != nil {
return nil, err
}

if err := config.SaveInstallConfig(cfg, store.ConfigPath()); err != nil {
return nil, err
}
log.Printf("Saved install config to %s", store.ConfigPath())

return cfg, nil
}

func runCreateCluster(cmd *cobra.Command, args []string) error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

// 1. Setup initial store (may be temporary before we know cluster name).
if assetDir == "" {
assetDir = "/tmp/_microshift_temp"
}
store, err := asset.NewStore(assetDir)
if err != nil {
return fmt.Errorf("setting up asset directory: %w", err)
}

// 2. Load or create install config.
cfg, err := loadOrCreateConfig(store)
if err != nil {
return err
}

// 3. Validate and get platform.
if err := cfg.Validate(); err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
plat := platform.New(cfg)
if plat == nil {
return fmt.Errorf("unsupported cloud provider: %s", cfg.CloudProvider)
}

// 4. Load or create state tracker.
statePath := filepath.Join(store.Dir(), stateFileName)
st, err := state.Load(statePath)
if err != nil {
return fmt.Errorf("loading state: %w", err)
}

fmt.Println()
fmt.Println("=== Starting cluster creation ===")
fmt.Printf(" Cluster: %s\n", cfg.Metadata.Name)
fmt.Printf(" Platform: %s\n", cfg.CloudProvider)
fmt.Printf(" Asset dir: %s\n", store.Dir())
fmt.Println()

// 5. Create infrastructure via platform SDK.
info, err := plat.Create(ctx, cfg, st)
if err != nil {
return fmt.Errorf("cluster creation failed: %w", err)
}

// 6. Save state.
if err := st.Save(); err != nil {
return fmt.Errorf("saving state: %w", err)
}

fmt.Println()
fmt.Println("=== Cluster created successfully ===")
fmt.Printf("%-25s %s\n", "Node Public IP:", info.NodePublicIP)
fmt.Printf("%-25s %s\n", "SSH Command:", info.SSHCommand)
fmt.Printf("%-25s %s\n", "Kubeconfig Retrieval:", info.KubeconfigCmd)
if info.DNSFQDN != "" {
fmt.Printf("%-25s %s\n", "DNS FQDN:", info.DNSFQDN)
}
fmt.Println()

return nil
}

func runDestroyCluster(cmd *cobra.Command, args []string) error {
if assetDir == "" {
return fmt.Errorf("--dir is required for destroy")
}

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

store, err := asset.NewStore(assetDir)
if err != nil {
return fmt.Errorf("setting up asset directory: %w", err)
}

// Load install config first — it has the correct region/project settings.
var cfg *types.InstallConfig
if store.ConfigExists() {
cfg, err = config.LoadInstallConfig(store.ConfigPath())
if err != nil {
return err
}
fmt.Printf("Using install config from %s\n", store.ConfigPath())
}

statePath := filepath.Join(store.Dir(), stateFileName)
st, err := state.Load(statePath)
if err != nil {
return fmt.Errorf("loading state: %w", err)
}
if len(st.Resources) == 0 {
return fmt.Errorf("no cluster resources found in %s (nothing to destroy)", statePath)
}

// Determine platform — prefer install config for region/project, fall back to state.
plat := platformFromState(cfg, st)
if plat == nil {
return fmt.Errorf("unable to determine platform from state file")
}

fmt.Println("Destroying cluster resources...")
if err := plat.Destroy(ctx, st); err != nil {
return fmt.Errorf("cluster destruction failed: %w", err)
}

fmt.Println("Cluster destroyed successfully.")
return nil
}

func platformFromState(cfg *types.InstallConfig, st *state.State) platform.Platform {
provider := ""
for _, r := range st.All() {
if r.Provider == "aws" || r.Provider == "gcp" {
provider = r.Provider
break
}
}
if provider == "" {
return nil
}

// If install config exists, use it directly for all settings.
if cfg != nil {
return platform.New(cfg)
}

// Fallback: minimal config from state (region/project won't be accurate).
return platform.New(&types.InstallConfig{
CloudProvider: provider,
Platform: types.Platform{
AWS: &types.AWSPlatform{Region: "us-east-2"},
GCP: &types.GCPPlatform{Project: "", Zone: "us-central1-a"},
},
})
}
Loading