Skip to content

Commit b4c379a

Browse files
committed
feat(controller): Implement controller commands
1 parent b0590e3 commit b4c379a

6 files changed

Lines changed: 624 additions & 0 deletions

File tree

cmd/controller/controller.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package controller
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
var ControllerCmd = &cobra.Command{
8+
Use: "controller",
9+
Short: "Manage morpher controller",
10+
Long: `Manage morpher controller including ping, status, and info operations.`,
11+
}
12+
13+
func init() {
14+
ControllerCmd.AddCommand(pingCmd, infoCmd)
15+
}

cmd/controller/info.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"morpherctl/internal/controller"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var infoCmd = &cobra.Command{
13+
Use: "info",
14+
Short: "Get controller information",
15+
Long: `Get detailed information about the morpher controller.`,
16+
RunE: func(_ *cobra.Command, _ []string) error {
17+
return getControllerInfo()
18+
},
19+
}
20+
21+
func getControllerInfo() error {
22+
// Create controller client.
23+
client, timeout, err := controller.CreateControllerClient()
24+
if err != nil {
25+
return fmt.Errorf("failed to create controller client: %w", err)
26+
}
27+
28+
fmt.Printf("Getting controller information: %s\n", client.GetBaseURL())
29+
30+
// Get controller info.
31+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
32+
defer cancel()
33+
34+
response, err := client.GetInfo(ctx)
35+
if err != nil {
36+
return fmt.Errorf("failed to get controller info: %w", err)
37+
}
38+
39+
// Display response.
40+
if response.Success {
41+
fmt.Println("Successfully retrieved controller information")
42+
43+
// Display detailed information if available.
44+
if response.Result != nil {
45+
fmt.Println("\nController Details:")
46+
fmt.Printf(" OS: %s %s (%s)\n",
47+
response.Result.OS.Name,
48+
response.Result.OS.PlatformVersion,
49+
response.Result.OS.KernelVersion)
50+
fmt.Printf(" Go Version: %s\n", response.Result.GoVersion)
51+
fmt.Printf(" Uptime: %s\n", response.Result.UpTime)
52+
} else {
53+
fmt.Println(" No detailed information available")
54+
}
55+
} else {
56+
fmt.Printf("Failed to get controller information: %d\n", response.StatusCode)
57+
}
58+
59+
return nil
60+
}

cmd/controller/ping.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"morpherctl/internal/controller"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var pingCmd = &cobra.Command{
13+
Use: "ping",
14+
Short: "Ping the controller",
15+
Long: `Send a ping request to the morpher controller to check connectivity.`,
16+
RunE: func(_ *cobra.Command, _ []string) error {
17+
return pingController()
18+
},
19+
}
20+
21+
func pingController() error {
22+
// Create controller client.
23+
client, timeout, err := controller.CreateControllerClient()
24+
if err != nil {
25+
return fmt.Errorf("failed to create controller client: %w", err)
26+
}
27+
28+
fmt.Printf("Sending ping request to controller: %s\n", client.GetBaseURL())
29+
30+
// Send ping request.
31+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
32+
defer cancel()
33+
34+
response, err := client.Ping(ctx)
35+
if err != nil {
36+
return fmt.Errorf("failed to connect to controller: %w", err)
37+
}
38+
39+
// Display response.
40+
if response.Success {
41+
fmt.Println("Controller responded successfully")
42+
if response.ResponseTime != "" {
43+
fmt.Printf("Response time: %v\n", response.ResponseTime)
44+
}
45+
} else {
46+
fmt.Printf("Controller responded but with unexpected status code: %d\n", response.StatusCode)
47+
}
48+
49+
return nil
50+
}

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/spf13/cobra"
77

88
"morpherctl/cmd/config"
9+
"morpherctl/cmd/controller"
910
"morpherctl/cmd/version"
1011
)
1112

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

3031
rootCmd.AddCommand(version.VersionCmd)
3132
rootCmd.AddCommand(config.ConfigCmd)
33+
rootCmd.AddCommand(controller.ControllerCmd)
3234
}

internal/controller/client.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"time"
9+
10+
"morpherctl/internal/config"
11+
)
12+
13+
const (
14+
defaultControllerURL = "http://localhost:9000"
15+
defaultTimeout = "30s"
16+
)
17+
18+
// Client handles communication with the morpher controller.
19+
type Client struct {
20+
baseURL string
21+
timeout time.Duration
22+
httpClient *http.Client
23+
token string
24+
}
25+
26+
// PingResponse represents the response from a ping request.
27+
type PingResponse struct {
28+
StatusCode int `json:"status_code"`
29+
ResponseTime string `json:"response_time,omitempty"`
30+
Success bool `json:"success"`
31+
}
32+
33+
// OSInfo represents operating system information.
34+
type OSInfo struct {
35+
Name string `json:"Name"`
36+
PlatformName string `json:"PlatformName"`
37+
PlatformVersion string `json:"PlatformVersion"`
38+
KernelVersion string `json:"KernelVersion"`
39+
}
40+
41+
// InfoResult represents the result data in info response.
42+
type InfoResult struct {
43+
OS OSInfo `json:"OS"`
44+
GoVersion string `json:"GoVersion"`
45+
UpTime string `json:"UpTime"`
46+
}
47+
48+
// InfoResponse represents the response from an info request.
49+
type InfoResponse struct {
50+
StatusCode int `json:"status_code"`
51+
Success bool `json:"success"`
52+
Result *InfoResult `json:"result,omitempty"`
53+
}
54+
55+
// NewClient creates a new controller client.
56+
func NewClient(baseURL string, timeout time.Duration, token string) *Client {
57+
if timeout == 0 {
58+
timeout = 30 * time.Second
59+
}
60+
61+
return &Client{
62+
baseURL: baseURL,
63+
timeout: timeout,
64+
httpClient: &http.Client{
65+
Timeout: timeout,
66+
},
67+
token: token,
68+
}
69+
}
70+
71+
// GetControllerConfig retrieves common configuration values for controller commands.
72+
func GetControllerConfig() (string, time.Duration, string, error) {
73+
// Get configuration values.
74+
configMgr := config.NewManager("")
75+
76+
controllerURL, err := configMgr.GetString("controller.url")
77+
if err != nil {
78+
controllerURL = defaultControllerURL
79+
}
80+
81+
timeoutStr, err := configMgr.GetString("controller.timeout")
82+
if err != nil {
83+
timeoutStr = defaultTimeout
84+
}
85+
86+
timeout, err := time.ParseDuration(timeoutStr)
87+
if err != nil {
88+
timeout = 30 * time.Second
89+
}
90+
91+
token, err := configMgr.GetString("auth.token")
92+
if err != nil {
93+
token = ""
94+
}
95+
96+
return controllerURL, timeout, token, nil
97+
}
98+
99+
// CreateControllerClient creates a new controller client with configuration.
100+
func CreateControllerClient() (*Client, time.Duration, error) {
101+
controllerURL, timeout, token, err := GetControllerConfig()
102+
if err != nil {
103+
return nil, 0, fmt.Errorf("failed to get controller configuration: %w", err)
104+
}
105+
106+
client := NewClient(controllerURL, timeout, token)
107+
return client, timeout, nil
108+
}
109+
110+
// newRequest creates a new HTTP request with context and authorization.
111+
func (c *Client) newRequest(ctx context.Context, method, path string) (*http.Request, error) {
112+
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, nil)
113+
if err != nil {
114+
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
115+
}
116+
117+
if c.token != "" {
118+
req.Header.Set("Authorization", "Bearer "+c.token)
119+
}
120+
121+
return req, nil
122+
}
123+
124+
// Ping sends a ping request to the controller.
125+
func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
126+
req, err := c.newRequest(ctx, "GET", "/ping")
127+
if err != nil {
128+
return nil, fmt.Errorf("failed to create ping request: %w", err)
129+
}
130+
131+
resp, err := c.httpClient.Do(req)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to send ping request: %w", err)
134+
}
135+
defer resp.Body.Close()
136+
137+
response := &PingResponse{
138+
StatusCode: resp.StatusCode,
139+
ResponseTime: resp.Header.Get("X-Response-Time"),
140+
Success: resp.StatusCode == http.StatusOK,
141+
}
142+
143+
return response, nil
144+
}
145+
146+
// GetInfo retrieves detailed controller information.
147+
func (c *Client) GetInfo(ctx context.Context) (*InfoResponse, error) {
148+
req, err := c.newRequest(ctx, "GET", "/info")
149+
if err != nil {
150+
return nil, fmt.Errorf("failed to create info request: %w", err)
151+
}
152+
153+
resp, err := c.httpClient.Do(req)
154+
if err != nil {
155+
return nil, fmt.Errorf("failed to get controller info: %w", err)
156+
}
157+
defer resp.Body.Close()
158+
159+
response := &InfoResponse{
160+
StatusCode: resp.StatusCode,
161+
Success: resp.StatusCode == http.StatusOK,
162+
}
163+
164+
// If the request was successful, try to parse the response body.
165+
if resp.StatusCode == http.StatusOK {
166+
var result InfoResult
167+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
168+
return response, fmt.Errorf("failed to parse controller info response: %w", err)
169+
}
170+
response.Result = &result
171+
}
172+
173+
return response, nil
174+
}
175+
176+
// IsHealthy checks if the controller is healthy based on ping response.
177+
func (c *Client) IsHealthy(ctx context.Context) (bool, error) {
178+
response, err := c.Ping(ctx)
179+
if err != nil {
180+
return false, err
181+
}
182+
return response.Success, nil
183+
}
184+
185+
// GetBaseURL returns the base URL of the controller.
186+
func (c *Client) GetBaseURL() string {
187+
return c.baseURL
188+
}
189+
190+
// GetTimeout returns the timeout setting of the client.
191+
func (c *Client) GetTimeout() time.Duration {
192+
return c.timeout
193+
}

0 commit comments

Comments
 (0)