Skip to content
Draft
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
7 changes: 7 additions & 0 deletions api/cluster_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ func clusterManagerPut(state state.State, r *http.Request) response.Response {
}
}

if args.ReverseTunnel != nil {
err = database.StoreClusterManagerConfig(state, r.Context(), name, database.ReverseTunnelKey, *args.ReverseTunnel)
if err != nil {
return response.SmartError(err)
}
}

return response.SyncResponse(true, nil)
}

Expand Down
30 changes: 30 additions & 0 deletions api/types/cluster_manager.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package types

import (
"sync"
"time"

"github.com/gorilla/websocket"
)

// ClusterManagersPost represents the cluster manager configuration when receiving a POST request in MicroCloud.
Expand Down Expand Up @@ -55,6 +58,10 @@ type ClusterManagerPut struct {
// Interval in seconds to send status messages to the cluster manager
// Example: 60
UpdateInterval *string `json:"update_interval" yaml:"update_interval"`

// Enables or disables the reverse tunnel to the cluster manager
// Example: true, false
ReverseTunnel *string `json:"reverse_tunnel" yaml:"reverse_tunnel"`
}

// StatusDistribution represents the distribution of items.
Expand Down Expand Up @@ -85,3 +92,26 @@ type ClusterManagerJoin struct {
ClusterCertificate string `json:"cluster_certificate" yaml:"cluster_certificate"`
Token string `json:"token" yaml:"token"`
}

// ClusterManagerTunnel represents the tunnel connection the cluster manager.
type ClusterManagerTunnel struct {
Mu sync.RWMutex
WsConn *websocket.Conn
}

// ClusterManagerTunnelRequest represents the request received through the tunnel.
type ClusterManagerTunnelRequest struct {
ID string `json:"id"`
Method string `json:"method"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
}

// ClusterManagerTunnelResponse represents the response sent through the tunnel.
type ClusterManagerTunnelResponse struct {
ID string `json:"id"`
Status int `json:"status"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
}
40 changes: 35 additions & 5 deletions client/cluster_manager_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/version"
"github.com/gorilla/websocket"

"github.com/canonical/microcloud/microcloud/api/types"
"github.com/canonical/microcloud/microcloud/database"
Expand Down Expand Up @@ -83,6 +85,26 @@ func (c *ClusterManagerClient) Delete(clusterCert *shared.CertInfo) error {
return err
}

// ConnectTunnelWebsocket establishes a WebSocket connection to the cluster manager for reverse tunneling.
func (c *ClusterManagerClient) ConnectTunnelWebsocket(clusterCert *shared.CertInfo) (*websocket.Conn, error) {
tlsConfig, address, err := c.getTlsConfig(clusterCert)
if err != nil {
return nil, fmt.Errorf("Failed to get TLS config: %w", err)
}

dialer := websocket.Dialer{
TLSClientConfig: tlsConfig,
}

u := url.URL{Scheme: "wss", Host: address, Path: "/1.0/remote-cluster/ws"}
conn, _, err := dialer.Dial(u.String(), nil)
if err != nil {
return nil, err
}

return conn, nil
}

func (c *ClusterManagerClient) craftRequest(method string, path string, reqBody io.Reader) (*http.Request, error) {
url := "https://remote" + path // remote is a placeholder, real address will be set in sendRequest
req, err := http.NewRequest(method, url, reqBody)
Expand Down Expand Up @@ -121,7 +143,19 @@ func (c *ClusterManagerClient) sendRequest(clusterCert *shared.CertInfo, req *ht

func (c *ClusterManagerClient) getHTTPClient(clusterCert *shared.CertInfo) (*http.Client, string, error) {
client := &http.Client{}
tlsConfig, address, err := c.getTlsConfig(clusterCert)
if err != nil {
return nil, "", fmt.Errorf("Failed to get TLS config: %w", err)
}

client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}

return client, address, nil
}

func (c *ClusterManagerClient) getTlsConfig(clusterCert *shared.CertInfo) (*tls.Config, string, error) {
var address string
var remoteCert *x509.Certificate
var err error
Expand Down Expand Up @@ -170,9 +204,5 @@ func (c *ClusterManagerClient) getHTTPClient(clusterCert *shared.CertInfo) (*htt
return &cert, nil
}

client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}

return client, address, nil
return tlsConfig, address, nil
}
15 changes: 14 additions & 1 deletion cmd/microcloud/cluster_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ func (c *cmdClusterManagerGet) run(_ *cobra.Command, args []string) error {
fmt.Printf("%s\n", clusterManager.StatusLastErrorTime)
case "status-last-error-response":
fmt.Printf("%s\n", clusterManager.StatusLastErrorResponse)
case "reverse-tunnel":
value, ok := clusterManager.Config[database.ReverseTunnelKey]
if ok {
fmt.Printf("%s\n", value)
}

default:
return errors.New("Invalid key")
}
Expand All @@ -258,7 +264,8 @@ func (c *cmdClusterManagerSet) command() *cobra.Command {
cmd.Short = "Set specific cluster manager configuration key."
cmd.Example = cli.FormatSection("", `microcloud cluster-manager set addresses example.com:8443
microcloud cluster-manager set certificate-fingerprint abababababababababababababababababababababababababababababababab
microcloud cluster-manager set update-interval-seconds 50`)
microcloud cluster-manager set update-interval-seconds 50
microcloud cluster-manager set reverse-tunnel true`)

cmd.RunE = c.run

Expand Down Expand Up @@ -287,6 +294,12 @@ func (c *cmdClusterManagerSet) run(_ *cobra.Command, args []string) error {
payload.CertificateFingerprint = &value
case "update-interval-seconds":
payload.UpdateInterval = &value
case "reverse-tunnel":
if value != "true" && value != "false" {
return errors.New("Invalid value for reverse-tunnel, expected 'true' or 'false'")
}

payload.ReverseTunnel = &value
default:
return errors.New("Invalid key")
}
Expand Down
Loading
Loading