Skip to content

Commit 0e6a734

Browse files
authored
Use TLS configuration for admin console as well (#2196)
1 parent 057996b commit 0e6a734

File tree

17 files changed

+305
-135
lines changed

17 files changed

+305
-135
lines changed

api/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Embedded Cluster API Package
2+
3+
This package provides the core API functionality for the Embedded Cluster system. It handles installation, authentication, console access, and health monitoring of the cluster.
4+
5+
## Package Structure
6+
7+
### Root Level
8+
The root directory contains the main API setup files and request handlers.
9+
10+
### Subpackages
11+
12+
#### `/controllers`
13+
Contains the business logic for different API endpoints. Each controller package focuses on a specific domain of functionality (e.g., authentication, console, installation) and implements the core business logic for that domain.
14+
15+
#### `/types`
16+
Defines the core data structures and types used throughout the API. This includes:
17+
- Request and response types
18+
- Domain models
19+
- Custom error types
20+
- Shared interfaces
21+
22+
#### `/pkg`
23+
Contains shared utilities and helper packages that provide common functionality used across different parts of the API. This includes both general-purpose utilities and domain-specific helpers.
24+
25+
#### `/client`
26+
Provides a client library for interacting with the API. The client package implements a clean interface for making API calls and handling responses, making it easy to integrate with the API from other parts of the system.
27+
28+
## Where to Add New Functionality
29+
30+
1. **New API Endpoints**:
31+
- Add route definitions in the root API setup
32+
- Create corresponding controller in `/controllers`
33+
- Define request/response types in `/types`
34+
35+
2. **New Business Logic**:
36+
- Place in appropriate controller under `/controllers`
37+
- Share common logic in `/pkg` if used across multiple controllers
38+
39+
3. **New Types/Models**:
40+
- Add to `/types` directory
41+
- Include validation and serialization methods
42+
43+
4. **New Client Methods**:
44+
- Add to appropriate file in `/client`
45+
- Include corresponding tests
46+
47+
5. **New Utilities**:
48+
- Place in `/pkg/utils` if general purpose
49+
- Create new subpackage under `/pkg` if domain-specific
50+
51+
## Best Practices
52+
53+
1. **Error Handling**:
54+
- Use custom error types from `/types`
55+
- Include proper error wrapping and context
56+
- Maintain consistent error handling patterns
57+
58+
2. **Testing**:
59+
- Write unit tests for all new functionality
60+
- Include integration tests for API endpoints
61+
- Maintain high test coverage
62+
63+
3. **Documentation**:
64+
- Document all public types and functions
65+
- Include examples for complex operations
66+
- Keep README updated with new functionality
67+
68+
4. **Logging**:
69+
- Use the logging utilities from the root package
70+
- Include appropriate log levels and context
71+
- Follow consistent logging patterns
72+
73+
## Architecture Decisions
74+
75+
1. **Release Metadata Independence**:
76+
- The EC API should not use the release metadata embedded into the EC binary (CLI)
77+
- This design choice enables better testability and easier iteration in the development environment
78+
- API components should be independently configurable and testable
79+
80+
## Integration
81+
82+
The API package is designed to be used as part of the larger Embedded Cluster system. It provides both HTTP endpoints for external access and a client library for internal use.
83+
84+
For integration examples and usage patterns, refer to the integration tests in the `/integration` directory.

api/api.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package api
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"net/http"
@@ -110,10 +111,44 @@ func (a *API) RegisterRoutes(router *mux.Router) {
110111
consoleRouter.HandleFunc("/available-network-interfaces", a.getListAvailableNetworkInterfaces).Methods("GET")
111112
}
112113

113-
func handleError(w http.ResponseWriter, err error) {
114+
func (a *API) JSON(w http.ResponseWriter, r *http.Request, code int, payload any) {
115+
response, err := json.Marshal(payload)
116+
if err != nil {
117+
a.logError(r, err, "failed to encode response")
118+
w.WriteHeader(http.StatusInternalServerError)
119+
return
120+
}
121+
122+
w.Header().Set("Content-Type", "application/json")
123+
w.WriteHeader(code)
124+
w.Write(response)
125+
}
126+
127+
func (a *API) JSONError(w http.ResponseWriter, r *http.Request, err error) {
114128
var apiErr *types.APIError
115129
if !errors.As(err, &apiErr) {
116130
apiErr = types.NewInternalServerError(err)
117131
}
118-
apiErr.JSON(w)
132+
133+
response, err := json.Marshal(apiErr)
134+
if err != nil {
135+
a.logError(r, err, "failed to encode response")
136+
w.WriteHeader(http.StatusInternalServerError)
137+
return
138+
}
139+
140+
w.Header().Set("Content-Type", "application/json")
141+
w.WriteHeader(apiErr.StatusCode)
142+
w.Write(response)
143+
}
144+
145+
func (a *API) logError(r *http.Request, err error, args ...any) {
146+
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).Error(args...)
147+
}
148+
149+
func logrusFieldsFromRequest(r *http.Request) logrus.Fields {
150+
return logrus.Fields{
151+
"method": r.Method,
152+
"path": r.URL.Path,
153+
}
119154
}

api/auth.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,14 @@ func (a *API) authMiddleware(next http.Handler) http.Handler {
2323
token := r.Header.Get("Authorization")
2424
if token == "" {
2525
err := errors.New("authorization header is required")
26-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
27-
Error("failed to authenticate")
26+
a.logError(r, err, "failed to authenticate")
2827
types.NewUnauthorizedError(err).JSON(w)
2928
return
3029
}
3130

3231
if !strings.HasPrefix(token, "Bearer ") {
3332
err := errors.New("authorization header must start with Bearer ")
34-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
35-
Error("failed to authenticate")
33+
a.logError(r, err, "failed to authenticate")
3634
types.NewUnauthorizedError(err).JSON(w)
3735
return
3836
}
@@ -41,8 +39,7 @@ func (a *API) authMiddleware(next http.Handler) http.Handler {
4139

4240
err := a.authController.ValidateToken(r.Context(), token)
4341
if err != nil {
44-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
45-
Error("failed to validate token")
42+
a.logError(r, err, "failed to validate token")
4643
types.NewUnauthorizedError(err).JSON(w)
4744
return
4845
}
@@ -55,8 +52,7 @@ func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) {
5552
var request AuthRequest
5653
err := json.NewDecoder(r.Body).Decode(&request)
5754
if err != nil {
58-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
59-
Error("failed to decode auth request")
55+
a.logError(r, err, "failed to decode auth request")
6056
types.NewBadRequestError(err).JSON(w)
6157
return
6258
}
@@ -68,8 +64,7 @@ func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) {
6864
}
6965

7066
if err != nil {
71-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
72-
Error("failed to authenticate")
67+
a.logError(r, err, "failed to authenticate")
7368
types.NewInternalServerError(err).JSON(w)
7469
return
7570
}

api/client/client.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ import (
99
"github.com/replicatedhq/embedded-cluster/api/types"
1010
)
1111

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-
1812
type Client interface {
1913
Login(password string) error
2014
GetInstall() (*types.Install, error)
@@ -51,7 +45,7 @@ func New(apiURL string, opts ...ClientOption) Client {
5145
}
5246

5347
if c.httpClient == nil {
54-
c.httpClient = defaultHTTPClient
48+
c.httpClient = http.DefaultClient
5549
}
5650

5751
return c

api/client/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestNew(t *testing.T) {
1919
clientImpl, ok := c.(*client)
2020
assert.True(t, ok, "Expected c to be of type *client")
2121
assert.Equal(t, "http://example.com", clientImpl.apiURL)
22-
assert.Equal(t, defaultHTTPClient, clientImpl.httpClient)
22+
assert.Equal(t, http.DefaultClient, clientImpl.httpClient)
2323
assert.Empty(t, clientImpl.token)
2424

2525
// Test with custom HTTP client

api/console.go

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package api
22

33
import (
4-
"encoding/json"
54
"net/http"
65

76
"github.com/replicatedhq/embedded-cluster/api/types"
@@ -14,23 +13,16 @@ type getBrandingResponse struct {
1413
func (a *API) getBranding(w http.ResponseWriter, r *http.Request) {
1514
branding, err := a.consoleController.GetBranding()
1615
if err != nil {
17-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
18-
Error("failed to get branding")
19-
handleError(w, err)
16+
a.logError(r, err, "failed to get branding")
17+
a.JSONError(w, r, err)
2018
return
2119
}
2220

2321
response := getBrandingResponse{
2422
Branding: branding,
2523
}
2624

27-
w.Header().Set("Content-Type", "application/json")
28-
w.WriteHeader(http.StatusOK)
29-
err = json.NewEncoder(w).Encode(response)
30-
if err != nil {
31-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
32-
Error("failed to encode branding")
33-
}
25+
a.JSON(w, r, http.StatusOK, response)
3426
}
3527

3628
type getListAvailableNetworkInterfacesResponse struct {
@@ -40,9 +32,8 @@ type getListAvailableNetworkInterfacesResponse struct {
4032
func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.Request) {
4133
interfaces, err := a.consoleController.ListAvailableNetworkInterfaces()
4234
if err != nil {
43-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
44-
Error("failed to list available network interfaces")
45-
handleError(w, err)
35+
a.logError(r, err, "failed to list available network interfaces")
36+
a.JSONError(w, r, err)
4637
return
4738
}
4839

@@ -54,11 +45,5 @@ func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.R
5445
NetworkInterfaces: interfaces,
5546
}
5647

57-
w.Header().Set("Content-Type", "application/json")
58-
w.WriteHeader(http.StatusOK)
59-
err = json.NewEncoder(w).Encode(response)
60-
if err != nil {
61-
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
62-
Error("failed to encode available network interfaces")
63-
}
48+
a.JSON(w, r, http.StatusOK, response)
6449
}

api/controllers/auth/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type Controller interface {
1313
ValidateToken(ctx context.Context, token string) error
1414
}
1515

16-
var _ Controller = &AuthController{}
16+
var _ Controller = (*AuthController)(nil)
1717

1818
type AuthController struct {
1919
password string

api/controllers/console/controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ type Controller interface {
1313
ListAvailableNetworkInterfaces() ([]string, error)
1414
}
1515

16+
var _ Controller = (*ConsoleController)(nil)
17+
1618
type ConsoleController struct {
1719
utils.NetUtils
1820
}

api/controllers/install/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type Controller interface {
1616
ReadStatus(ctx context.Context) (*types.InstallationStatus, error)
1717
}
1818

19-
var _ Controller = &InstallController{}
19+
var _ Controller = (*InstallController)(nil)
2020

2121
type InstallController struct {
2222
installationManager installation.InstallationManager

0 commit comments

Comments
 (0)