Skip to content

Commit ec30def

Browse files
committed
Milestone 1
1 parent 4d1d294 commit ec30def

File tree

93 files changed

+9657
-284
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+9657
-284
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ go.work.sum
1818
/local-dev/
1919
*.tmp
2020
.envrc
21+
.DS_Store
2122

2223
# Ignore preflight bundles generated during local dev
2324
preflightbundle*

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ list-distros:
337337
.PHONY: create-node%
338338
create-node%: DISTRO = debian-bookworm
339339
create-node%: NODE_PORT = 30000
340+
create-node%: MANAGER_NODE_PORT = 30080
340341
create-node%: K0S_DATA_DIR = /var/lib/embedded-cluster/k0s
341342
create-node%:
342343
@docker run -d \
@@ -348,7 +349,9 @@ create-node%:
348349
-v $(shell pwd):/replicatedhq/embedded-cluster \
349350
-v $(shell dirname $(shell pwd))/kots:/replicatedhq/kots \
350351
$(if $(filter node0,node$*),-p $(NODE_PORT):$(NODE_PORT)) \
352+
$(if $(filter node0,node$*),-p $(MANAGER_NODE_PORT):$(MANAGER_NODE_PORT)) \
351353
$(if $(filter node0,node$*),-p 30003:30003) \
354+
-e EC_PUBLIC_ADDRESS=localhost \
352355
replicated/ec-distro:$(DISTRO)
353356

354357
@$(MAKE) ssh-node$*

api/api.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package api
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/gorilla/mux"
9+
"github.com/sirupsen/logrus"
10+
11+
"github.com/replicatedhq/embedded-cluster/api/controllers/auth"
12+
"github.com/replicatedhq/embedded-cluster/api/controllers/console"
13+
"github.com/replicatedhq/embedded-cluster/api/controllers/install"
14+
"github.com/replicatedhq/embedded-cluster/api/types"
15+
)
16+
17+
type API struct {
18+
authController auth.Controller
19+
consoleController console.Controller
20+
installController install.Controller
21+
configChan chan<- *types.InstallationConfig
22+
logger logrus.FieldLogger
23+
}
24+
25+
type APIOption func(*API)
26+
27+
func WithAuthController(authController auth.Controller) APIOption {
28+
return func(a *API) {
29+
a.authController = authController
30+
}
31+
}
32+
33+
func WithConsoleController(consoleController console.Controller) APIOption {
34+
return func(a *API) {
35+
a.consoleController = consoleController
36+
}
37+
}
38+
39+
func WithInstallController(installController install.Controller) APIOption {
40+
return func(a *API) {
41+
a.installController = installController
42+
}
43+
}
44+
45+
func WithLogger(logger logrus.FieldLogger) APIOption {
46+
return func(a *API) {
47+
a.logger = logger
48+
}
49+
}
50+
51+
func WithConfigChan(configChan chan<- *types.InstallationConfig) APIOption {
52+
return func(a *API) {
53+
a.configChan = configChan
54+
}
55+
}
56+
57+
func New(password string, opts ...APIOption) (*API, error) {
58+
api := &API{}
59+
for _, opt := range opts {
60+
opt(api)
61+
}
62+
63+
if api.authController == nil {
64+
authController, err := auth.NewAuthController(password)
65+
if err != nil {
66+
return nil, fmt.Errorf("new auth controller: %w", err)
67+
}
68+
api.authController = authController
69+
}
70+
71+
if api.consoleController == nil {
72+
consoleController, err := console.NewConsoleController()
73+
if err != nil {
74+
return nil, fmt.Errorf("new console controller: %w", err)
75+
}
76+
api.consoleController = consoleController
77+
}
78+
79+
if api.installController == nil {
80+
installController, err := install.NewInstallController()
81+
if err != nil {
82+
return nil, fmt.Errorf("new install controller: %w", err)
83+
}
84+
api.installController = installController
85+
}
86+
87+
if api.logger == nil {
88+
api.logger = NewDiscardLogger()
89+
}
90+
91+
return api, nil
92+
}
93+
94+
func (a *API) RegisterRoutes(router *mux.Router) {
95+
router.HandleFunc("/health", a.getHealth).Methods("GET")
96+
97+
router.HandleFunc("/auth/login", a.postAuthLogin).Methods("POST")
98+
router.HandleFunc("/branding", a.getBranding).Methods("GET")
99+
100+
authenticatedRouter := router.PathPrefix("").Subrouter()
101+
authenticatedRouter.Use(a.authMiddleware)
102+
103+
installRouter := authenticatedRouter.PathPrefix("/install").Subrouter()
104+
installRouter.HandleFunc("", a.getInstall).Methods("GET")
105+
installRouter.HandleFunc("/config", a.setInstallConfig).Methods("POST")
106+
installRouter.HandleFunc("/status", a.setInstallStatus).Methods("POST")
107+
installRouter.HandleFunc("/status", a.getInstallStatus).Methods("GET")
108+
109+
consoleRouter := authenticatedRouter.PathPrefix("/console").Subrouter()
110+
consoleRouter.HandleFunc("/available-network-interfaces", a.getListAvailableNetworkInterfaces).Methods("GET")
111+
}
112+
113+
func handleError(w http.ResponseWriter, err error) {
114+
var apiErr *types.APIError
115+
if !errors.As(err, &apiErr) {
116+
apiErr = types.NewInternalServerError(err)
117+
}
118+
apiErr.JSON(w)
119+
}

api/auth.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
"strings"
8+
9+
"github.com/replicatedhq/embedded-cluster/api/controllers/auth"
10+
"github.com/replicatedhq/embedded-cluster/api/types"
11+
)
12+
13+
type AuthRequest struct {
14+
Password string `json:"password"`
15+
}
16+
17+
type AuthResponse struct {
18+
Token string `json:"token"`
19+
}
20+
21+
func (a *API) authMiddleware(next http.Handler) http.Handler {
22+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23+
token := r.Header.Get("Authorization")
24+
if token == "" {
25+
err := errors.New("authorization header is required")
26+
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
27+
Error("failed to authenticate")
28+
types.NewUnauthorizedError(err).JSON(w)
29+
return
30+
}
31+
32+
if !strings.HasPrefix(token, "Bearer ") {
33+
err := errors.New("authorization header must start with Bearer ")
34+
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
35+
Error("failed to authenticate")
36+
types.NewUnauthorizedError(err).JSON(w)
37+
return
38+
}
39+
40+
token = token[len("Bearer "):]
41+
42+
err := a.authController.ValidateToken(r.Context(), token)
43+
if err != nil {
44+
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
45+
Error("failed to validate token")
46+
types.NewUnauthorizedError(err).JSON(w)
47+
return
48+
}
49+
50+
next.ServeHTTP(w, r)
51+
})
52+
}
53+
54+
func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) {
55+
var request AuthRequest
56+
err := json.NewDecoder(r.Body).Decode(&request)
57+
if err != nil {
58+
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
59+
Error("failed to decode auth request")
60+
types.NewBadRequestError(err).JSON(w)
61+
return
62+
}
63+
64+
token, err := a.authController.Authenticate(r.Context(), request.Password)
65+
if errors.Is(err, auth.ErrInvalidPassword) {
66+
types.NewUnauthorizedError(err).JSON(w)
67+
return
68+
}
69+
70+
if err != nil {
71+
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
72+
Error("failed to authenticate")
73+
types.NewInternalServerError(err).JSON(w)
74+
return
75+
}
76+
77+
response := AuthResponse{
78+
Token: token,
79+
}
80+
81+
json.NewEncoder(w).Encode(response)
82+
}

api/client/auth.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
)
8+
9+
// Login sends a login request to the API server with the provided password and retrieves a session token. The token is stored in the client struct for subsequent requests.
10+
func (c *client) Login(password string) error {
11+
loginReq := struct {
12+
Password string `json:"password"`
13+
}{
14+
Password: password,
15+
}
16+
17+
b, err := json.Marshal(loginReq)
18+
if err != nil {
19+
return err
20+
}
21+
22+
req, err := http.NewRequest("POST", c.apiURL+"/api/auth/login", bytes.NewBuffer(b))
23+
if err != nil {
24+
return err
25+
}
26+
req.Header.Set("Content-Type", "application/json")
27+
28+
resp, err := c.httpClient.Do(req)
29+
if err != nil {
30+
return err
31+
}
32+
defer resp.Body.Close()
33+
34+
if resp.StatusCode != http.StatusOK {
35+
return errorFromResponse(resp)
36+
}
37+
38+
var loginResp struct {
39+
Token string `json:"token"`
40+
}
41+
err = json.NewDecoder(resp.Body).Decode(&loginResp)
42+
if err != nil {
43+
return err
44+
}
45+
46+
c.token = loginResp.Token
47+
return nil
48+
}

api/client/client.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package client
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
9+
"github.com/replicatedhq/embedded-cluster/api/types"
10+
)
11+
12+
var defaultHTTPClient = &http.Client{
13+
Transport: &http.Transport{
14+
Proxy: nil, // This is a local client so no proxy is needed
15+
},
16+
}
17+
18+
type Client interface {
19+
Login(password string) error
20+
GetInstall() (*types.Install, error)
21+
SetInstallConfig(config types.InstallationConfig) (*types.Install, error)
22+
SetInstallStatus(status types.InstallationStatus) (*types.Install, error)
23+
}
24+
25+
type client struct {
26+
apiURL string
27+
httpClient *http.Client
28+
token string
29+
}
30+
31+
type ClientOption func(*client)
32+
33+
func WithHTTPClient(httpClient *http.Client) ClientOption {
34+
return func(c *client) {
35+
c.httpClient = httpClient
36+
}
37+
}
38+
39+
func WithToken(token string) ClientOption {
40+
return func(c *client) {
41+
c.token = token
42+
}
43+
}
44+
45+
func New(apiURL string, opts ...ClientOption) Client {
46+
c := &client{
47+
apiURL: apiURL,
48+
}
49+
for _, opt := range opts {
50+
opt(c)
51+
}
52+
53+
if c.httpClient == nil {
54+
c.httpClient = defaultHTTPClient
55+
}
56+
57+
return c
58+
}
59+
60+
func setAuthorizationHeader(req *http.Request, token string) {
61+
if token != "" {
62+
req.Header.Set("Authorization", "Bearer "+token)
63+
}
64+
}
65+
66+
func errorFromResponse(resp *http.Response) error {
67+
body, err := io.ReadAll(resp.Body)
68+
if err != nil {
69+
return fmt.Errorf("unexpected response: status=%d", resp.StatusCode)
70+
}
71+
var apiError types.APIError
72+
err = json.Unmarshal(body, &apiError)
73+
if err != nil {
74+
return fmt.Errorf("unexpected response: status=%d, body=%q", resp.StatusCode, string(body))
75+
}
76+
return &apiError
77+
}

0 commit comments

Comments
 (0)