Skip to content

Commit

Permalink
Merge branch 'headlamp-k8s:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
vlasov-y authored Jan 7, 2025
2 parents 2b2fd63 + 4da085f commit f9bb57c
Show file tree
Hide file tree
Showing 12 changed files with 394 additions and 155 deletions.
17 changes: 16 additions & 1 deletion backend/cmd/multiplexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,14 @@ func (m *Multiplexer) sendIfNewResourceVersion(

// sendCompleteMessage sends a COMPLETE message to the client.
func (m *Multiplexer) sendCompleteMessage(conn *Connection, clientConn *websocket.Conn) error {
conn.mu.RLock()
if conn.closed {
conn.mu.RUnlock()
return nil // Connection is already closed, no need to send message
}

conn.mu.RUnlock()

completeMsg := Message{
ClusterID: conn.ClusterID,
Path: conn.Path,
Expand All @@ -593,7 +601,14 @@ func (m *Multiplexer) sendCompleteMessage(conn *Connection, clientConn *websocke
conn.writeMu.Lock()
defer conn.writeMu.Unlock()

return clientConn.WriteJSON(completeMsg)
err := clientConn.WriteJSON(completeMsg)
if err != nil {
logger.Log(logger.LevelInfo, nil, err, "connection closed while writing complete message")

return nil // Just return nil for any error - connection is dead anyway
}

return nil
}

// sendDataMessage sends the actual data message to the client.
Expand Down
62 changes: 61 additions & 1 deletion backend/cmd/multiplexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,67 @@ func TestSendCompleteMessage_ClosedConnection(t *testing.T) {
// Test with closed connection
clientConn.Close()
err = m.sendCompleteMessage(conn, clientConn)
assert.Error(t, err)
assert.NoError(t, err)
}

func TestSendCompleteMessage_ErrorConditions(t *testing.T) {
tests := []struct {
name string
setupConn func(*Connection, *websocket.Conn)
expectedError bool
}{
{
name: "connection already marked as closed",
setupConn: func(conn *Connection, _ *websocket.Conn) {
conn.closed = true
},
expectedError: false,
},
{
name: "normal closure",
setupConn: func(_ *Connection, clientConn *websocket.Conn) {
//nolint:errcheck
clientConn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
clientConn.Close()
},
expectedError: false,
},
{
name: "unexpected close error",
setupConn: func(_ *Connection, clientConn *websocket.Conn) {
//nolint:errcheck
clientConn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseProtocolError, ""))
clientConn.Close()
},
expectedError: false, // All errors return nil now
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMultiplexer(kubeconfig.NewContextStore())
clientConn, clientServer := createTestWebSocketConnection()
defer clientServer.Close()

conn := &Connection{
ClusterID: "test-cluster",
Path: "/api/v1/pods",
UserID: "test-user",
Query: "watch=true",
}

tt.setupConn(conn, clientConn)
err := m.sendCompleteMessage(conn, clientConn)

if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func createMockKubeAPIServer() *httptest.Server {
Expand Down
272 changes: 210 additions & 62 deletions charts/headlamp/README.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,231 @@
# headlamp
# Headlamp Helm Chart

Headlamp is an easy-to-use and extensible Kubernetes web UI.
Headlamp is an easy-to-use and extensible Kubernetes web UI that provides:
- 🚀 Modern, fast, and responsive interface
- 🔒 OIDC authentication support
- 🔌 Plugin system for extensibility
- 🎯 Real-time cluster state updates

**Homepage:** <https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp>
## Prerequisites

## TL;DR
- Kubernetes 1.21+
- Helm 3.x
- Cluster admin access for initial setup

## Quick Start

Add the Headlamp repository and install the chart:

```console
$ helm repo add headlamp https://headlamp-k8s.github.io/headlamp/
$ helm repo update
$ helm install my-headlamp headlamp/headlamp --namespace kube-system
```

Access Headlamp:
```console
$ kubectl port-forward -n kube-system svc/my-headlamp 8080:80
```
Then open http://localhost:8080 in your browser.

## Installation

### Basic Installation
```console
$ helm install my-headlamp headlamp/headlamp --namespace kube-system
```

### Installation with OIDC
```console
$ helm install my-headlamp headlamp/headlamp \
--namespace kube-system \
--set config.oidc.clientID=your-client-id \
--set config.oidc.clientSecret=your-client-secret \
--set config.oidc.issuerURL=https://your-issuer-url
```

### Installation with Ingress
```console
$ helm install my-headlamp headlamp/headlamp \
--namespace kube-system \
--set ingress.enabled=true \
--set ingress.hosts[0].host=headlamp.example.com \
--set ingress.hosts[0].paths[0].path=/
```

## Configuration

### Core Parameters

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| replicaCount | int | `1` | Number of desired pods |
| image.registry | string | `"ghcr.io"` | Container image registry |
| image.repository | string | `"headlamp-k8s/headlamp"` | Container image name |
| image.tag | string | `""` | Container image tag (defaults to Chart appVersion) |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |

### Application Configuration

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| config.baseURL | string | `""` | Base URL path for Headlamp UI |
| config.pluginsDir | string | `"/headlamp/plugins"` | Directory to load Headlamp plugins from |
| config.extraArgs | array | `[]` | Additional arguments for Headlamp server |

### OIDC Configuration

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| config.oidc.clientID | string | `""` | OIDC client ID |
| config.oidc.clientSecret | string | `""` | OIDC client secret |
| config.oidc.issuerURL | string | `""` | OIDC issuer URL |
| config.oidc.scopes | string | `""` | OIDC scopes to be used |
| config.oidc.secret.create | bool | `true` | Create OIDC secret using provided values |
| config.oidc.secret.name | string | `"oidc"` | Name of the OIDC secret |
| config.oidc.externalSecret.enabled | bool | `false` | Enable using external secret for OIDC |
| config.oidc.externalSecret.name | string | `""` | Name of external OIDC secret |

## Maintainers
There are three ways to configure OIDC:

See [MAINTAINERS.md](https://github.com/headlamp-k8s/headlamp/blob/main/MAINTAINERS.md) in the headlamp github repo.
1. Using direct configuration:
```yaml
config:
oidc:
clientID: "your-client-id"
clientSecret: "your-client-secret"
issuerURL: "https://your-issuer"
scopes: "openid profile email"
```
## Source Code
2. Using automatic secret creation:
```yaml
config:
oidc:
secret:
create: true
name: oidc
```
* <https://github.com/headlamp-k8s/headlamp>
* <https://headlamp.dev/>
3. Using external secret:
```yaml
config:
oidc:
secret:
create: false
externalSecret:
enabled: true
name: your-oidc-secret
```
### Headlamp parameters
### Deployment Configuration
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | Affinity settings for pod assignment |
| clusterRoleBinding.annotations | object | `{}` | Annotations to add to the cluster role binding |
| clusterRoleBinding.create | bool | `true` | Specified whether a cluster role binding should be created |
| clusterRoleBinding.clusterRoleName| string | `cluster-admin` | Kubernetes ClusterRole name |
| env | list | `[]` | An optional list of environment variables |
| fullnameOverride | string | `""` | Overrides the full name of the chart |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. One of Always, Never, IfNotPresent |
| replicaCount | int | `1` | Number of desired pods |
| image.registry | string | `"ghcr.io"` | Container image registry |
| image.repository | string | `"headlamp-k8s/headlamp"` | Container image name |
| image.tag | string | `""` | Container image tag, If "" uses appVersion in Chart.yaml |
| imagePullSecrets | list | `[]` | An optional list of references to secrets in the same namespace to use for pulling any of the images used |
| ingress.annotations | object | `{}` | Annotations for Ingress resource |
| ingress.enabled | bool | `false` | Enable ingress controller resource |
| ingress.ingressClassName | string | `""` | The ingress class name. Replacement for the deprecated "kubernetes.io/ingress.class" annotation |
| ingress.hosts | list | `[]` | Hostname(s) for the Ingress resource |
| image.tag | string | `""` | Container image tag (defaults to Chart appVersion) |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| imagePullSecrets | list | `[]` | Image pull secrets references |
| nameOverride | string | `""` | Override the name of the chart |
| fullnameOverride | string | `""` | Override the full name of the chart |
| initContainers | list | `[]` | Init containers to run before main container |

### Security Configuration

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| serviceAccount.create | bool | `true` | Create service account |
| serviceAccount.name | string | `""` | Service account name |
| serviceAccount.annotations | object | `{}` | Service account annotations |
| clusterRoleBinding.create | bool | `true` | Create cluster role binding |
| clusterRoleBinding.clusterRoleName | string | `"cluster-admin"` | Kubernetes ClusterRole name |
| clusterRoleBinding.annotations | object | `{}` | Cluster role binding annotations |
| podSecurityContext | object | `{}` | Pod security context (e.g., fsGroup: 2000) |
| securityContext.runAsNonRoot | bool | `true` | Run container as non-root |
| securityContext.privileged | bool | `false` | Run container in privileged mode |
| securityContext.runAsUser | int | `100` | User ID to run container |
| securityContext.runAsGroup | int | `101` | Group ID to run container |
| securityContext.capabilities | object | `{}` | Container capabilities (e.g., drop: [ALL]) |
| securityContext.readOnlyRootFilesystem | bool | `false` | Mount root filesystem as read-only |

### Storage Configuration

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| persistentVolumeClaim.enabled | bool | `false` | Enable PVC |
| persistentVolumeClaim.annotations | object | `{}` | PVC annotations |
| persistentVolumeClaim.size | string | `""` | PVC size (required if enabled) |
| persistentVolumeClaim.storageClassName | string | `""` | Storage class name |
| persistentVolumeClaim.accessModes | list | `[]` | PVC access modes |
| persistentVolumeClaim.selector | object | `{}` | PVC selector |
| persistentVolumeClaim.volumeMode | string | `""` | PVC volume mode |
| volumeMounts | list | `[]` | Container volume mounts |
| volumes | list | `[]` | Pod volumes |

### Network Configuration

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| service.type | string | `"ClusterIP"` | Kubernetes service type |
| service.port | int | `80` | Kubernetes service port |
| ingress.enabled | bool | `false` | Enable ingress |
| ingress.className | string | `""` | Ingress class name |
| ingress.annotations | object | `{}` | Ingress annotations (e.g., kubernetes.io/tls-acme: "true") |
| ingress.hosts | list | `[]` | Ingress hosts configuration |
| ingress.tls | list | `[]` | Ingress TLS configuration |
| initContainers | list | `[]` | An optional list of init containers to be run before the main containers. |
| nameOverride | string | `""` | Overrides the name of the chart |

Example ingress configuration:
```yaml
ingress:
enabled: true
annotations:
kubernetes.io/tls-acme: "true"
hosts:
- host: headlamp.example.com
paths:
- path: /
type: ImplementationSpecific
tls:
- secretName: headlamp-tls
hosts:
- headlamp.example.com
```

### Resource Management

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| resources | object | `{}` | Container resource requests/limits |
| nodeSelector | object | `{}` | Node labels for pod assignment |
| persistentVolumeClaim.accessModes | list | `[]` | accessModes for the persistent volume claim, eg: ReadWriteOnce, ReadOnlyMany, ReadWriteMany etc. |
| persistentVolumeClaim.annotations | object | `{}` | Annotations to add to the persistent volume claim (if enabled) |
| persistentVolumeClaim.enabled | bool | `false` | Enable Persistent Volume Claim |
| persistentVolumeClaim.selector | object | `{}` | selector for the persistent volume claim. |
| persistentVolumeClaim.size | string | `""` | size of the persistent volume claim, eg: 10Gi. Required if enabled is true. |
| persistentVolumeClaim.storageClassName | string | `""` | storageClassName for the persistent volume claim. |
| persistentVolumeClaim.volumeMode | string | `""` | volumeMode for the persistent volume claim, eg: Filesystem, Block. |
| podAnnotations | object | `{}` | Annotations to add to the pod |
| podSecurityContext | object | `{}` | Headlamp pod's Security Context |
| replicaCount | int | `1` | Number of desired pods |
| resources | object | `{}` | CPU/Memory resource requests/limits |
| securityContext | object | `{}` | Headlamp containers Security Context |
| service.port | int | `80` | Kubernetes Service port |
| service.type | string | `"ClusterIP"` | Kubernetes Service type |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
| serviceAccount.name | string | `""` | The name of the service account to use.(If not set and create is true, a name is generated using the fullname template) |
| tolerations | list | `[]` | Toleration labels for pod assignment |
| volumeMounts | list | `[]` | Headlamp containers volume mounts |
| volumes | list | `[]` | Headlamp pod's volumes |


### Headlamp Configuration

| Key | Type | Default | Description |
|------------------------------------|--------|-----------------------|-------------------------------------------------------------------------------------------------------|
| config.baseURL | string | `""` | base url path at which headlamp should run |
| config.oidc.clientID | string | `""` | OIDC client ID |
| config.oidc.clientSecret | string | `""` | OIDC client secret |
| config.oidc.issuerURL | string | `""` | OIDC issuer URL |
| config.oidc.scopes | string | `""` | OIDC scopes to be used |
| config.oidc.secret.create | bool | `true` | Enable this option to have the chart automatically create the OIDC secret using the specified values. |
| config.oidc.secret.name | string | `oidc` | Name of the OIDC secret used by headlamp |
| config.oidc.externalSecret.enabled | bool | `false` | Enable this option if you want to use an external secret for OIDC configuration. |
| config.oidc.externalSecret.name | string | `""` | Name of the external OIDC secret to be used by headlamp. |
| config.pluginsDir | string | `"/headlamp/plugins"` | directory to look for plugins |
| config.extraArgs | array | `[]` | Extra arguments that can be given to the container |
| tolerations | list | `[]` | Pod tolerations |
| affinity | object | `{}` | Pod affinity settings |
| podAnnotations | object | `{}` | Pod annotations |
| env | list | `[]` | Additional environment variables |

Example resource configuration:
```yaml
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
```

Example environment variables:
```yaml
env:
- name: KUBERNETES_SERVICE_HOST
value: "localhost"
- name: KUBERNETES_SERVICE_PORT
value: "6443"
```

## Links

- [GitHub Repository](https://github.com/headlamp-k8s/headlamp)
- [Documentation](https://headlamp.dev/)
- [Maintainers](https://github.com/headlamp-k8s/headlamp/blob/main/MAINTAINERS.md)
Loading

0 comments on commit f9bb57c

Please sign in to comment.