Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - UDL-TF/UpdateController: The update controller takes care of updating the TF2 servers for the node. · GitHub
Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - UDL-TF/UpdateController: The update controller takes care of updating the TF2 servers for the node. · GitHub
Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - UDL-TF/UpdateController: The update controller takes care of updating the TF2 servers for the node. · GitHub
Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - UDL-TF/UpdateController: The update controller takes care of updating the TF2 servers for the node. · GitHub
Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - UDL-TF/UpdateController: The update controller takes care of updating the TF2 servers for the node. · GitHub
Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - UDL-TF/UpdateController: The update controller takes care of updating the TF2 servers for the node. · GitHub
Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - UDL-TF/UpdateController: The update controller takes care of updating the TF2 servers for the node. · GitHub
Skip to content

Repository files navigation

UpdateController

A Kubernetes controller written in Go that automatically manages updates for Team Fortress 2 (TF2) game servers running in a cluster. The controller monitors for game updates using SteamCMD, applies updates when available, and orchestrates pod restarts to ensure servers run the latest version.

Go VersionLicense

Overview

The UpdateController is a Kubernetes-native controller that solves the challenge of keeping game servers up-to-date in a containerized environment. It leverages the ghcr.io/udl-tf/tf2-image container which includes SteamCMD for update management.

Key Responsibilities

  • Monitor: Periodically check for TF2 server updates using SteamCMD
  • Update: Download and apply updates to mounted game directories
  • Restart: Intelligently restart affected Kubernetes workloads (Deployments, StatefulSets, etc.)
  • Handle Errors: Gracefully manage update failures with retry logic

Architecture

graph TB
subgraph "Kubernetes Cluster"
UC[UpdateController<br/>Go Service]
K8S_API[Kubernetes API]
subgraph "Game Server Workloads"
TF2_POD1[TF2 Server Pod 1]
TF2_POD2[TF2 Server Pod 2]
TF2_POD3[TF2 Server Pod 3]
end
PVC[Persistent Volume<br/>Game Files]
end
STEAM[Steam CDN<br/>Update Server]
UC -->|Check Updates| STEAM
UC -->|Read/Write| PVC
UC -->|List/Restart Pods| K8S_API
K8S_API -->|Restart| TF2_POD1
K8S_API -->|Restart| TF2_POD2
K8S_API -->|Restart| TF2_POD3
TF2_POD1 -.->|Mount| PVC
TF2_POD2 -.->|Mount| PVC
TF2_POD3 -.->|Mount| PVC
style UC fill:#4a90e2,stroke:#2e5c8a,color:#fff
style STEAM fill:#1b2838,stroke:#000,color:#fff
style PVC fill:#326ce5,stroke:#1a4d99,color:#fff
Loading

How It Works

Update Check Flow

sequenceDiagram
autonumber
participant UC as UpdateController
participant SC as SteamCMD
participant PV as Persistent Volume
participant K8S as Kubernetes API
participant POD as TF2 Server Pods
UC->>UC: Start Periodic Check<br/>(Configurable Interval)
alt Game Not Installed
UC->>PV: Check Game Directory
PV-->>UC: Not Found
UC->>UC: Flag as Update Needed<br/>(Initial Installation)
else Game Installed
UC->>PV: Read Local Manifest<br/>(appmanifest_*.acf)
PV-->>UC: Installed Build ID
UC->>SC: Query App Info (app_info_print)
SC-->>UC: Latest Build ID
UC->>UC: Compare Build IDs
end
alt Update/Install Needed
UC->>SC: Execute Update Script
SC->>PV: Download & Apply Update
alt Update Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>SC: Validate Installation
SC->>PV: Verify Game Files
PV-->>SC: Validation Complete
SC-->>UC: Validation Success
UC->>K8S: Query Pods by Selector
K8S-->>UC: Return Matching Pods
UC->>UC: Determine Pod Owners
UC->>K8S: Restart Workloads<br/>(Deployments/StatefulSets/etc.)
K8S->>POD: Rolling Restart
POD->>PV: Mount Updated Files
UC->>UC: Log Success & Reset Retry Count
else 0x6 Error Detected
SC-->>UC: State 0x6 Error
UC->>UC: Detect 0x6 Error Pattern
UC->>PV: Clear steamapps Directory
PV-->>UC: Cleared
UC->>SC: Retry Update Script
SC->>PV: Download & Apply Update
alt Retry Success
PV-->>SC: Update Complete
SC-->>UC: Success
UC->>K8S: Restart Workloads
else Retry Failed
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Next Retry
end
else Other Update Failure
SC-->>UC: Failure
UC->>UC: Log Error & Increment Retry
UC->>UC: Wait Before Retry
end
else Already Up-to-Date
UC->>UC: Continue Monitoring
end
Loading

Controller State Machine

stateDiagram-v2
[*] --> Idle
Idle --> CheckingInstallation: Check Timer Triggered
CheckingInstallation --> InitialInstall: Game Not Installed
CheckingInstallation --> ComparingBuildIDs: Game Installed
InitialInstall --> Downloading: Start Initial Install
ComparingBuildIDs --> ReadLocalManifest: Get Installed Build ID
ReadLocalManifest --> QuerySteamAPI: Get Latest Build ID
QuerySteamAPI --> UpdateAvailable: Build IDs Differ
QuerySteamAPI --> Idle: Build IDs Match (Up-to-Date)
UpdateAvailable --> Downloading: Start Update
Downloading --> Installing: Download Complete
Downloading --> Error0x6Detected: State 0x6 Error
Downloading --> Failed: Other Download Error
Error0x6Detected --> ClearingSteamApps: Remove steamapps Directory
ClearingSteamApps --> Downloading: Retry After Cleanup
Installing --> Validating: Install Complete
Installing --> Error0x6Detected: State 0x6 Error
Installing --> Failed: Install Error
Validating --> DeterminingOwners: Validation Success
Validating --> Failed: Validation Error
DeterminingOwners --> RestartingWorkloads: Find Pod Owners
RestartingWorkloads --> Success: All Workloads Restarted
RestartingWorkloads --> Failed: Restart Error
Success --> Idle: Wait for Next Check
Failed --> Retry: Retry Count < Max
Failed --> Idle: Max Retries Exceeded
Retry --> Downloading: Retry Update
Loading

Features

  • Automatic Update Detection: Leverages SteamCMD to detect when TF2 updates are available using build ID comparison (no unnecessary downloads)
  • Initial Installation Support: Automatically detects and performs initial game installation if not present
  • Build ID Tracking: Compares local manifest build IDs with Steam's latest build IDs for efficient update detection
  • 0x6 Error Recovery: Automatic detection and recovery from Steam's 0x6 state errors by clearing and retrying
  • Smart Pod Selection: Restart pods based on:
    • Label selectors (e.g., app=tf2-server)
    • Workload ownership detection
  • Multiple Workload Support: Handles Deployments, StatefulSets, DaemonSets, and ReplicaSets
  • Error Handling: Configurable retry logic with exponential backoff
  • Update Validation: Verifies update success before restarting pods
  • Zero-Downtime Updates: Utilizes Kubernetes rolling restart mechanisms
  • Observability: Structured logging with klog for detailed operation tracking

Prerequisites

  • Kubernetes cluster (v1.25+)
  • Go 1.25+ (for development)
  • Access to ghcr.io/udl-tf/tf2-image
  • Persistent Volume for game files
  • RBAC permissions for pod/deployment management

Installation

Using Helm (Recommended)

# Install the UpdateController from OCI registry
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--namespace game-servers \
--create-namespace \
--set image.tag=latest \
--set config.checkInterval=30m
# Or specify a version
helm install update-controller oci://ghcr.io/udl-tf/helm/update-controller \
--version 0.1.0 \
--namespace game-servers \
--create-namespace

Using kubectl

# Apply the controller manifest
kubectl apply -f https://raw.githubusercontent.com/UDL-TF/UpdateController/main/deploy/controller.yaml

From Source

# Clone the repository
git clone https://github.com/UDL-TF/UpdateController.git
cd UpdateController
# Build the controller
go build -o update-controller ./cmd/controller
# Run locally (for development)
./update-controller --kubeconfig=$HOME/.kube/config

Configuration

Environment Variables

VariableDescriptionDefaultRequired
CHECK_INTERVALInterval between update checks30mNo
STEAMCMD_PATHPath to SteamCMD executable/home/steam/steamcmdNo
STEAMAPPSteam app name (TF2)tfNo
STEAMAPPIDSteam app ID232250No
GAME_MOUNT_PATHPath where game files are mounted/tfNo
UPDATE_SCRIPTName of the update scripttf_update.txtNo
POD_SELECTORLabel selector for TF2 podsapp=tf2-serverYes
MAX_RETRIESMaximum update retry attempts3No
RETRY_DELAYDelay between retries5mNo
NAMESPACEKubernetes namespace to watchdefaultNo

RBAC Configuration

The controller requires the following permissions:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:
name: update-controllerrules:
- apiGroups: ['']resources: ['pods']verbs: ['get', 'list', 'watch']
- apiGroups: ['apps']resources: ['deployments', 'statefulsets', 'daemonsets', 'replicasets']verbs: ['get', 'list', 'patch']
- apiGroups: ['']resources: ['persistentvolumeclaims']verbs: ['get', 'list']

Development

Project Structure

UpdateController/
├── cmd/
│ └── controller/ # Main controller application
│ └── main.go
├── internal/
│ ├── controller/ # Controller logic
│ │ ├── update.go # Update check & apply
│ │ ├── restart.go # Pod restart logic
│ │ └── config.go # Configuration
│ ├── steamcmd/ # SteamCMD integration
│ │ └── client.go
│ └── k8s/ # Kubernetes client wrappers
│ └── client.go
├── deploy/ # Kubernetes manifests
│ ├── controller.yaml
│ └── rbac.yaml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Building

# Build for your platform
go build -o update-controller ./cmd/controller
# Build Docker image
docker build -t ghcr.io/udl-tf/update-controller:latest .# Run tests
go test ./...
# Run with race detection
go test -race ./...

Local Development

# Install dependencies
go mod download
# Run controller locally
go run ./cmd/controller --kubeconfig=$HOME/.kube/config
# Enable debug loggingexport LOG_LEVEL=debug
go run ./cmd/controller

License

See LICENSE file for details.

Dependencies

About

The update controller takes care of updating the TF2 servers for the node.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages