Skip to content

Commit ff9c1f2

Browse files
DanNiEShclaude
andcommitted
Add Host controller with Ironic power management
Implement a controller that watches Host CRs (from osac-operator) and reconciles spec.online to Ironic power on/off. Filters on spec.hostManagementClass == "openstack" and syncs status.poweredOn. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6cfb356 commit ff9c1f2

12 files changed

Lines changed: 706 additions & 251 deletions

File tree

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ jobs:
2020
- name: Run linter
2121
uses: golangci/golangci-lint-action@v8
2222
with:
23-
version: v2.1.0
23+
version: v2.7.2

.github/workflows/test-e2e.yml

Lines changed: 0 additions & 34 deletions
This file was deleted.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.18.0
185185
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
186186
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
187187
ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
188-
GOLANGCI_LINT_VERSION ?= v2.1.0
188+
GOLANGCI_LINT_VERSION ?= v2.7.2
189189

190190
.PHONY: kustomize
191191
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.

README.md

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,131 @@
1-
# public_template
1+
# host-management-openstack
22

3-
Use this repository as the template for public-facing repositories. Please keep the following warning at the top of the repository README:
3+
Kubernetes controller that manages bare metal nodes via OpenStack Ironic. Watches Host CRs (defined in [osac-operator](https://github.com/DanNiESh/osac-operator)) and reconciles the desired state to Ironic.
44

5-
> [!WARNING]
6-
> Be mindful of the content you commit to this repository. Do not commit any
7-
> material containing Red Hat confidential content, including information about
8-
> future product development plans.
5+
## Description
6+
7+
This controller filters Host CRs where `spec.hostManagementClass == "openstack"` and uses `spec.id` as the Ironic node UUID to manage node lifecycle through Ironic.
8+
9+
## Test locally (with Ironic)
10+
11+
Run the host controller on your machine and point it at your Ironic API. You need a Kubernetes API (e.g. a `kind` cluster) so the controller can watch Host CRs; the controller runs locally and talks to Ironic from your local machine.
12+
13+
### 1. Create a kind cluster
14+
15+
```bash
16+
kind create cluster --name host-mgmt-test
17+
```
18+
19+
### 2. Install the Host CRD from osac-operator
20+
21+
The Host CR is defined in the [osac-operator](https://github.com/DanNiESh/osac-operator) `host-operator` branch.
22+
23+
```bash
24+
kubectl apply -f <path-to-osac-operator>/config/crd/bases/osac.openshift.io_hosts.yaml
25+
```
26+
27+
### 3. Set Ironic URL and token
28+
29+
```bash
30+
export IRONIC_URL="https://ironic-url"
31+
export OSAC_AUTH_TOKEN="token" # e.g. openstack token issue -f value -c id
32+
export IRONIC_INSECURE=true # optional, if using self-signed certs
33+
```
34+
35+
### 4. Run the controller
36+
37+
```bash
38+
make run
39+
```
40+
41+
The controller will use `IRONIC_URL` and `OSAC_AUTH_TOKEN` to connect to Ironic.
42+
43+
### 5. Apply a sample Host CR
44+
45+
In another terminal:
46+
47+
```bash
48+
kubectl apply -f config/samples/v1alpha1_host.yaml
49+
```
50+
51+
### 6. Verify
52+
53+
```bash
54+
# Check Host status
55+
kubectl get host host-sample -n osac-namespace -o yaml
56+
57+
# Watch controller logs for reconciliation
58+
```
59+
60+
## Getting Started
61+
62+
### Prerequisites
63+
- go version v1.24.0+
64+
- docker version 17.03+
65+
- kubectl version v1.11.3+
66+
- Access to a Kubernetes v1.11.3+ cluster
67+
- Access to an OpenStack Ironic endpoint
68+
69+
### To Deploy on the cluster
70+
**Build and push your image to the location specified by `IMG`:**
71+
72+
```sh
73+
make docker-build docker-push IMG=<some-registry>/host-management-openstack:tag
74+
```
75+
76+
**Install the Host CRD (from osac-operator):**
77+
78+
```sh
79+
kubectl apply -f <path-to-osac-operator>/config/crd/bases/osac.openshift.io_hosts.yaml
80+
```
81+
82+
**Deploy the Manager to the cluster with the image specified by `IMG`:**
83+
84+
```sh
85+
make deploy IMG=<some-registry>/host-management-openstack:tag
86+
```
87+
88+
> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
89+
privileges or be logged in as admin.
90+
91+
**Create instances of your solution**
92+
You can apply the samples (examples) from the config/sample:
93+
94+
```sh
95+
kubectl apply -k config/samples/
96+
```
97+
98+
### To Uninstall
99+
**Delete the instances (CRs) from the cluster:**
100+
101+
```sh
102+
kubectl delete -k config/samples/
103+
```
104+
105+
**UnDeploy the controller from the cluster:**
106+
107+
```sh
108+
make undeploy
109+
```
110+
111+
## Contributing
112+
113+
**NOTE:** Run `make help` for more information on all potential `make` targets
114+
115+
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
116+
117+
## License
118+
119+
Copyright 2026.
120+
121+
Licensed under the Apache License, Version 2.0 (the "License");
122+
you may not use this file except in compliance with the License.
123+
You may obtain a copy of the License at
124+
125+
http://www.apache.org/licenses/LICENSE-2.0
126+
127+
Unless required by applicable law or agreed to in writing, software
128+
distributed under the License is distributed on an "AS IS" BASIS,
129+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
130+
See the License for the specific language governing permissions and
131+
limitations under the License.

cmd/main.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ import (
3636
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
3737
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
3838
"sigs.k8s.io/controller-runtime/pkg/webhook"
39+
40+
"github.com/osac-project/host-management-openstack/internal/controller"
41+
"github.com/osac-project/host-management-openstack/pkg/ironic"
42+
osacopenshiftiov1alpha1 "github.com/osac-project/osac-operator/api/v1alpha1"
3943
// +kubebuilder:scaffold:imports
4044
)
4145

@@ -46,7 +50,7 @@ var (
4650

4751
func init() {
4852
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
49-
53+
utilruntime.Must(osacopenshiftiov1alpha1.AddToScheme(scheme))
5054
// +kubebuilder:scaffold:scheme
5155
}
5256

@@ -198,6 +202,34 @@ func main() {
198202
os.Exit(1)
199203
}
200204

205+
// Ironic client for bare metal power management
206+
var ironicClient *ironic.Client
207+
if ironicURLStr := os.Getenv("IRONIC_URL"); ironicURLStr != "" {
208+
authToken := os.Getenv("OSAC_AUTH_TOKEN")
209+
if authToken == "" {
210+
setupLog.Error(nil, "OSAC_AUTH_TOKEN required when IRONIC_URL is set")
211+
os.Exit(1)
212+
}
213+
ironicOpts := ironic.ClientOptions{InsecureSkipVerify: os.Getenv("IRONIC_INSECURE") == "true"}
214+
var ironicErr error
215+
ironicClient, ironicErr = ironic.NewClientWithToken(ironicURLStr, authToken, ironicOpts)
216+
if ironicErr != nil {
217+
setupLog.Error(ironicErr, "failed to create Ironic client")
218+
os.Exit(1)
219+
}
220+
setupLog.Info("Ironic client configured", "url", ironicURLStr)
221+
} else {
222+
setupLog.Info("IRONIC_URL not set")
223+
}
224+
225+
if err := (&controller.HostReconciler{
226+
Client: mgr.GetClient(),
227+
Scheme: mgr.GetScheme(),
228+
IronicClient: ironicClient,
229+
}).SetupWithManager(mgr); err != nil {
230+
setupLog.Error(err, "unable to create controller", "controller", "Host")
231+
os.Exit(1)
232+
}
201233
// +kubebuilder:scaffold:builder
202234

203235
if metricsCertWatcher != nil {

config/manager/manager.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ spec:
6666
image: controller:latest
6767
name: manager
6868
ports: []
69+
env:
70+
- name: IRONIC_URL
71+
valueFrom:
72+
secretKeyRef:
73+
name: ironic-credentials
74+
key: url
75+
optional: true
76+
- name: OSAC_AUTH_TOKEN
77+
valueFrom:
78+
secretKeyRef:
79+
name: ironic-credentials
80+
key: token
81+
optional: true
82+
- name: IRONIC_INSECURE
83+
value: "false"
6984
securityContext:
7085
allowPrivilegeEscalation: false
7186
capabilities:

config/rbac/role.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ rules:
99
- apiGroups: [""]
1010
resources: ["pods"]
1111
verbs: ["get", "list", "watch"]
12+
- apiGroups: ["osac.openshift.io"]
13+
resources: ["hosts"]
14+
verbs: ["get", "list", "watch"]
15+
- apiGroups: ["osac.openshift.io"]
16+
resources: ["hosts/status"]
17+
verbs: ["get", "update", "patch"]

config/samples/v1alpha1_host.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: osac.openshift.io/v1alpha1
2+
kind: Host
3+
metadata:
4+
name: host-sample
5+
namespace: osac-namespace
6+
spec:
7+
hostClass: fc830
8+
id: 519d6b37-13a9-444c-9a2a-bcac8a33dc44
9+
name: host-sample
10+
hostManagementClass: openstack
11+
networkClass: openstack
12+
templateID: default_template
13+
online: true
14+
networkInterfaces:
15+
- macAddress: aa:bb:cc:dd:ee:f1
16+
network: private-vlan-network

0 commit comments

Comments
 (0)