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
15 changes: 15 additions & 0 deletions cmd/controller/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package controller

import (
"github.com/spf13/cobra"
)

var ControllerCmd = &cobra.Command{
Use: "controller",
Short: "Manage morpher controller",
Long: `Manage morpher controller including ping, status, and info operations.`,
}

func init() {
ControllerCmd.AddCommand(pingCmd, infoCmd)
}
60 changes: 60 additions & 0 deletions cmd/controller/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package controller

import (
"context"
"fmt"

"morpherctl/internal/controller"

"github.com/spf13/cobra"
)

var infoCmd = &cobra.Command{
Use: "info",
Short: "Get controller information",
Long: `Get detailed information about the morpher controller.`,
RunE: func(_ *cobra.Command, _ []string) error {
return getControllerInfo()
},
}

func getControllerInfo() error {
// Create controller client.
client, timeout, err := controller.CreateControllerClient()
if err != nil {
return fmt.Errorf("failed to create controller client: %w", err)
}

fmt.Printf("Getting controller information: %s\n", client.GetBaseURL())

// Get controller info.
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

response, err := client.GetInfo(ctx)
if err != nil {
return fmt.Errorf("failed to get controller info: %w", err)
}

// Display response.
if response.Success {
fmt.Println("Successfully retrieved controller information")

// Display detailed information if available.
if response.Result != nil {
fmt.Println("\nController Details:")
fmt.Printf(" OS: %s %s (%s)\n",
response.Result.OS.Name,
response.Result.OS.PlatformVersion,
response.Result.OS.KernelVersion)
fmt.Printf(" Go Version: %s\n", response.Result.GoVersion)
fmt.Printf(" Uptime: %s\n", response.Result.UpTime)
} else {
fmt.Println(" No detailed information available")
}
} else {
fmt.Printf("Failed to get controller information: %d\n", response.StatusCode)
}

return nil
}
50 changes: 50 additions & 0 deletions cmd/controller/ping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package controller

import (
"context"
"fmt"

"morpherctl/internal/controller"

"github.com/spf13/cobra"
)

var pingCmd = &cobra.Command{
Use: "ping",
Short: "Ping the controller",
Long: `Send a ping request to the morpher controller to check connectivity.`,
RunE: func(_ *cobra.Command, _ []string) error {
return pingController()
},
}

func pingController() error {
// Create controller client.
client, timeout, err := controller.CreateControllerClient()
if err != nil {
return fmt.Errorf("failed to create controller client: %w", err)
}

fmt.Printf("Sending ping request to controller: %s\n", client.GetBaseURL())

// Send ping request.
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

response, err := client.Ping(ctx)
if err != nil {
return fmt.Errorf("failed to connect to controller: %w", err)
}

// Display response.
if response.Success {
fmt.Println("Controller responded successfully")
if response.ResponseTime != "" {
fmt.Printf("Response time: %v\n", response.ResponseTime)
}
} else {
fmt.Printf("Controller responded but with unexpected status code: %d\n", response.StatusCode)
}

return nil
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/spf13/cobra"

"morpherctl/cmd/config"
"morpherctl/cmd/controller"
"morpherctl/cmd/version"
)

Expand All @@ -29,4 +30,5 @@ func init() {

rootCmd.AddCommand(version.VersionCmd)
rootCmd.AddCommand(config.ConfigCmd)
rootCmd.AddCommand(controller.ControllerCmd)
}
193 changes: 193 additions & 0 deletions internal/controller/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package controller

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"morpherctl/internal/config"
)

const (
defaultControllerURL = "http://localhost:9000"
defaultTimeout = "30s"
)

// Client handles communication with the morpher controller.
type Client struct {
baseURL string
timeout time.Duration
httpClient *http.Client
token string
}

// PingResponse represents the response from a ping request.
type PingResponse struct {
StatusCode int `json:"status_code"`
ResponseTime string `json:"response_time,omitempty"`
Success bool `json:"success"`
}

// OSInfo represents operating system information.
type OSInfo struct {
Name string `json:"Name"`
PlatformName string `json:"PlatformName"`
PlatformVersion string `json:"PlatformVersion"`
KernelVersion string `json:"KernelVersion"`
}

// InfoResult represents the result data in info response.
type InfoResult struct {
OS OSInfo `json:"OS"`
GoVersion string `json:"GoVersion"`
UpTime string `json:"UpTime"`
}

// InfoResponse represents the response from an info request.
type InfoResponse struct {
StatusCode int `json:"status_code"`
Success bool `json:"success"`
Result *InfoResult `json:"result,omitempty"`
}

// NewClient creates a new controller client.
func NewClient(baseURL string, timeout time.Duration, token string) *Client {
if timeout == 0 {
timeout = 30 * time.Second
}

return &Client{
baseURL: baseURL,
timeout: timeout,
httpClient: &http.Client{
Timeout: timeout,
},
token: token,
}
}

// GetControllerConfig retrieves common configuration values for controller commands.
func GetControllerConfig() (string, time.Duration, string, error) {
// Get configuration values.
configMgr := config.NewManager("")

controllerURL, err := configMgr.GetString("controller.url")
if err != nil {
controllerURL = defaultControllerURL
}

timeoutStr, err := configMgr.GetString("controller.timeout")
if err != nil {
timeoutStr = defaultTimeout
}

timeout, err := time.ParseDuration(timeoutStr)
if err != nil {
timeout = 30 * time.Second
}

token, err := configMgr.GetString("auth.token")
if err != nil {
token = ""
}

return controllerURL, timeout, token, nil
}

// CreateControllerClient creates a new controller client with configuration.
func CreateControllerClient() (*Client, time.Duration, error) {
controllerURL, timeout, token, err := GetControllerConfig()
if err != nil {
return nil, 0, fmt.Errorf("failed to get controller configuration: %w", err)
}

client := NewClient(controllerURL, timeout, token)
return client, timeout, nil
}

// newRequest creates a new HTTP request with context and authorization.
func (c *Client) newRequest(ctx context.Context, method, path string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}

if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}

return req, nil
}

// Ping sends a ping request to the controller.
func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
req, err := c.newRequest(ctx, "GET", "/ping")
if err != nil {
return nil, fmt.Errorf("failed to create ping request: %w", err)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send ping request: %w", err)
}
defer resp.Body.Close()

response := &PingResponse{
StatusCode: resp.StatusCode,
ResponseTime: resp.Header.Get("X-Response-Time"),
Success: resp.StatusCode == http.StatusOK,
}

return response, nil
}

// GetInfo retrieves detailed controller information.
func (c *Client) GetInfo(ctx context.Context) (*InfoResponse, error) {
req, err := c.newRequest(ctx, "GET", "/info")
if err != nil {
return nil, fmt.Errorf("failed to create info request: %w", err)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get controller info: %w", err)
}
defer resp.Body.Close()

response := &InfoResponse{
StatusCode: resp.StatusCode,
Success: resp.StatusCode == http.StatusOK,
}

// If the request was successful, try to parse the response body.
if resp.StatusCode == http.StatusOK {
var result InfoResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return response, fmt.Errorf("failed to parse controller info response: %w", err)
}
response.Result = &result
}

return response, nil
}

// IsHealthy checks if the controller is healthy based on ping response.
func (c *Client) IsHealthy(ctx context.Context) (bool, error) {
response, err := c.Ping(ctx)
if err != nil {
return false, err
}
return response.Success, nil
}

// GetBaseURL returns the base URL of the controller.
func (c *Client) GetBaseURL() string {
return c.baseURL
}

// GetTimeout returns the timeout setting of the client.
func (c *Client) GetTimeout() time.Duration {
return c.timeout
}
Loading
Loading