Skip to content

Use TLS configuration for admin console as well #2196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 27, 2025
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
84 changes: 84 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Embedded Cluster API Package

This package provides the core API functionality for the Embedded Cluster system. It handles installation, authentication, console access, and health monitoring of the cluster.

## Package Structure

### Root Level
The root directory contains the main API setup files and request handlers.

### Subpackages

#### `/controllers`
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.

#### `/types`
Defines the core data structures and types used throughout the API. This includes:
- Request and response types
- Domain models
- Custom error types
- Shared interfaces

#### `/pkg`
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.

#### `/client`
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.

## Where to Add New Functionality

1. **New API Endpoints**:
- Add route definitions in the root API setup
- Create corresponding controller in `/controllers`
- Define request/response types in `/types`

2. **New Business Logic**:
- Place in appropriate controller under `/controllers`
- Share common logic in `/pkg` if used across multiple controllers

3. **New Types/Models**:
- Add to `/types` directory
- Include validation and serialization methods

4. **New Client Methods**:
- Add to appropriate file in `/client`
- Include corresponding tests

5. **New Utilities**:
- Place in `/pkg/utils` if general purpose
- Create new subpackage under `/pkg` if domain-specific

## Best Practices

1. **Error Handling**:
- Use custom error types from `/types`
- Include proper error wrapping and context
- Maintain consistent error handling patterns

2. **Testing**:
- Write unit tests for all new functionality
- Include integration tests for API endpoints
- Maintain high test coverage

3. **Documentation**:
- Document all public types and functions
- Include examples for complex operations
- Keep README updated with new functionality

4. **Logging**:
- Use the logging utilities from the root package
- Include appropriate log levels and context
- Follow consistent logging patterns

## Architecture Decisions

1. **Release Metadata Independence**:
- The EC API should not use the release metadata embedded into the EC binary (CLI)
- This design choice enables better testability and easier iteration in the development environment
- API components should be independently configurable and testable

## Integration

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.

For integration examples and usage patterns, refer to the integration tests in the `/integration` directory.
39 changes: 37 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -110,10 +111,44 @@ func (a *API) RegisterRoutes(router *mux.Router) {
consoleRouter.HandleFunc("/available-network-interfaces", a.getListAvailableNetworkInterfaces).Methods("GET")
}

func handleError(w http.ResponseWriter, err error) {
func (a *API) JSON(w http.ResponseWriter, r *http.Request, code int, payload any) {
response, err := json.Marshal(payload)
if err != nil {
a.logError(r, err, "failed to encode response")
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(response)
}

func (a *API) JSONError(w http.ResponseWriter, r *http.Request, err error) {
var apiErr *types.APIError
if !errors.As(err, &apiErr) {
apiErr = types.NewInternalServerError(err)
}
apiErr.JSON(w)

response, err := json.Marshal(apiErr)
if err != nil {
a.logError(r, err, "failed to encode response")
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.StatusCode)
w.Write(response)
}

func (a *API) logError(r *http.Request, err error, args ...any) {
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).Error(args...)
}

func logrusFieldsFromRequest(r *http.Request) logrus.Fields {
return logrus.Fields{
"method": r.Method,
"path": r.URL.Path,
}
}
15 changes: 5 additions & 10 deletions api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,14 @@ func (a *API) authMiddleware(next http.Handler) http.Handler {
token := r.Header.Get("Authorization")
if token == "" {
err := errors.New("authorization header is required")
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
Error("failed to authenticate")
a.logError(r, err, "failed to authenticate")
types.NewUnauthorizedError(err).JSON(w)
return
}

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

err := a.authController.ValidateToken(r.Context(), token)
if err != nil {
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
Error("failed to validate token")
a.logError(r, err, "failed to validate token")
types.NewUnauthorizedError(err).JSON(w)
return
}
Expand All @@ -55,8 +52,7 @@ func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) {
var request AuthRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
Error("failed to decode auth request")
a.logError(r, err, "failed to decode auth request")
types.NewBadRequestError(err).JSON(w)
return
}
Expand All @@ -68,8 +64,7 @@ func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) {
}

if err != nil {
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
Error("failed to authenticate")
a.logError(r, err, "failed to authenticate")
types.NewInternalServerError(err).JSON(w)
return
}
Expand Down
8 changes: 1 addition & 7 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import (
"github.com/replicatedhq/embedded-cluster/api/types"
)

var defaultHTTPClient = &http.Client{
Transport: &http.Transport{
Proxy: nil, // This is a local client so no proxy is needed
},
}

type Client interface {
Login(password string) error
GetInstall() (*types.Install, error)
Expand Down Expand Up @@ -51,7 +45,7 @@ func New(apiURL string, opts ...ClientOption) Client {
}

if c.httpClient == nil {
c.httpClient = defaultHTTPClient
c.httpClient = http.DefaultClient
}

return c
Expand Down
2 changes: 1 addition & 1 deletion api/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestNew(t *testing.T) {
clientImpl, ok := c.(*client)
assert.True(t, ok, "Expected c to be of type *client")
assert.Equal(t, "http://example.com", clientImpl.apiURL)
assert.Equal(t, defaultHTTPClient, clientImpl.httpClient)
assert.Equal(t, http.DefaultClient, clientImpl.httpClient)
assert.Empty(t, clientImpl.token)

// Test with custom HTTP client
Expand Down
27 changes: 6 additions & 21 deletions api/console.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package api

import (
"encoding/json"
"net/http"

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

response := getBrandingResponse{
Branding: branding,
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(response)
if err != nil {
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
Error("failed to encode branding")
}
a.JSON(w, r, http.StatusOK, response)
}

type getListAvailableNetworkInterfacesResponse struct {
Expand All @@ -40,9 +32,8 @@ type getListAvailableNetworkInterfacesResponse struct {
func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.Request) {
interfaces, err := a.consoleController.ListAvailableNetworkInterfaces()
if err != nil {
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
Error("failed to list available network interfaces")
handleError(w, err)
a.logError(r, err, "failed to list available network interfaces")
a.JSONError(w, r, err)
return
}

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

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(response)
if err != nil {
a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).
Error("failed to encode available network interfaces")
}
a.JSON(w, r, http.StatusOK, response)
}
2 changes: 1 addition & 1 deletion api/controllers/auth/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Controller interface {
ValidateToken(ctx context.Context, token string) error
}

var _ Controller = &AuthController{}
var _ Controller = (*AuthController)(nil)

type AuthController struct {
password string
Expand Down
2 changes: 2 additions & 0 deletions api/controllers/console/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type Controller interface {
ListAvailableNetworkInterfaces() ([]string, error)
}

var _ Controller = (*ConsoleController)(nil)

type ConsoleController struct {
utils.NetUtils
}
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/install/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Controller interface {
ReadStatus(ctx context.Context) (*types.InstallationStatus, error)
}

var _ Controller = &InstallController{}
var _ Controller = (*InstallController)(nil)

type InstallController struct {
installationManager installation.InstallationManager
Expand Down
Loading
Loading