Skip to content

Commit 816f127

Browse files
committed
Example rewrite of ironic client
This rewrites `pkg/ironic/client.go` so that: - We remove all of the custom auth handling code and rely instead on standard OS_* variables. - We modify SetPowerState so that it doesn't accept bare string arguments. This allows us to check arguments at compile time.
1 parent ded9bec commit 816f127

5 files changed

Lines changed: 41 additions & 130 deletions

File tree

cmd/main.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,11 @@ func main() {
204204

205205
// Ironic client for bare metal power management
206206
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")
207+
if ironicClient, err = ironic.NewClient(); err != nil {
208+
setupLog.Error(err, "failed to create Ironic client")
209+
os.Exit(1)
223210
}
211+
setupLog.Info("Connect to ironic", "endpoint", ironicClient.GetEndpoint())
224212

225213
if err := (&controller.HostReconciler{
226214
Client: mgr.GetClient(),

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.25.0
55
require (
66
github.com/go-logr/logr v1.4.3
77
github.com/gophercloud/gophercloud/v2 v2.11.1
8+
github.com/gophercloud/utils/v2 v2.0.0-20260107124036-1d7954eb9711
89
github.com/onsi/ginkgo/v2 v2.28.1
910
github.com/onsi/gomega v1.39.0
1011
k8s.io/apimachinery v0.35.3
@@ -14,6 +15,7 @@ require (
1415

1516
require (
1617
github.com/Masterminds/semver/v3 v3.4.0 // indirect
18+
github.com/gofrs/uuid/v5 v5.4.0 // indirect
1719
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
1820
go.yaml.in/yaml/v2 v2.4.3 // indirect
1921
go.yaml.in/yaml/v3 v3.0.4 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
5454
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
5555
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
5656
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
57+
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
58+
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
5759
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
5860
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
5961
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -73,6 +75,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
7375
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7476
github.com/gophercloud/gophercloud/v2 v2.11.1 h1:jCs4vLH8sJgRqrPzqVfWgl7uI6JnIIlsgeIRM0uHjxY=
7577
github.com/gophercloud/gophercloud/v2 v2.11.1/go.mod h1:Rm0YvKQ4QYX2rY9XaDKnjRzSGwlG5ge4h6ABYnmkKQM=
78+
github.com/gophercloud/utils/v2 v2.0.0-20260107124036-1d7954eb9711 h1:LgYkb/jttJKwFwZHZjaT8zA/Ef4HMtrosABO/pnSaJI=
79+
github.com/gophercloud/utils/v2 v2.0.0-20260107124036-1d7954eb9711/go.mod h1:X6Plvu4Iot+ebr3g7tmw439sin+eGzDbO3tdjUOOP2I=
7680
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
7781
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
7882
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=

internal/controller/host_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,15 @@ func (r *HostReconciler) reconcilePower(ctx context.Context, host *osacopenshift
107107

108108
if host.Spec.Online && !currentlyOn {
109109
log.Info("Powering on node", "nodeID", host.Spec.ExternalID)
110-
if err := r.IronicClient.SetPowerState(ctx, host.Spec.ExternalID, "on"); err != nil {
110+
if err := r.IronicClient.SetPowerState(ctx, host.Spec.ExternalID, ironic.PowerOn); err != nil {
111111
log.Error(err, "failed to power on node", "nodeID", host.Spec.ExternalID)
112112
}
113113
return
114114
}
115115

116116
if !host.Spec.Online && currentlyOn {
117117
log.Info("Powering off node", "nodeID", host.Spec.ExternalID)
118-
if err := r.IronicClient.SetPowerState(ctx, host.Spec.ExternalID, "off"); err != nil {
118+
if err := r.IronicClient.SetPowerState(ctx, host.Spec.ExternalID, ironic.PowerOff); err != nil {
119119
log.Error(err, "failed to power off node", "nodeID", host.Spec.ExternalID)
120120
}
121121
return

pkg/ironic/client.go

Lines changed: 29 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -18,113 +18,42 @@ package ironic
1818

1919
import (
2020
"context"
21-
"crypto/tls"
2221
"fmt"
23-
"net/http"
24-
"strings"
25-
"time"
2622

2723
"github.com/gophercloud/gophercloud/v2"
28-
"github.com/gophercloud/gophercloud/v2/openstack/baremetal/noauth"
2924
"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes"
25+
"github.com/gophercloud/utils/v2/openstack/clientconfig"
3026
)
3127

32-
// microversion is the Ironic API version required for power management flows.
33-
const microversion = "1.69"
34-
3528
// Client talks to Ironic over REST via gophercloud.
36-
type Client struct {
37-
serviceClient *gophercloud.ServiceClient
38-
}
39-
40-
// ClientOptions configures the Ironic client.
41-
type ClientOptions struct {
42-
InsecureSkipVerify bool
43-
HTTPClient *http.Client
44-
}
45-
46-
type tokenRoundTripper struct {
47-
rt http.RoundTripper
48-
token string
49-
}
50-
51-
func (t *tokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
52-
req = req.Clone(req.Context())
53-
req.Header.Set("X-Auth-Token", t.token)
54-
return t.rt.RoundTrip(req)
55-
}
56-
57-
// ensureV1Suffix returns base URL with trailing /v1 for gophercloud noauth.
58-
func ensureV1Suffix(endpoint string) string {
59-
endpoint = strings.TrimSuffix(endpoint, "/")
60-
if strings.HasSuffix(endpoint, "/v1") {
61-
return endpoint
62-
}
63-
return endpoint + "/v1"
64-
}
65-
66-
// NewClient creates an Ironic client with no-auth (typical for in-cluster Ironic with no Keystone).
67-
func NewClient(ironicEndpoint string, opts ClientOptions) (*Client, error) {
68-
return newClient(ensureV1Suffix(ironicEndpoint), "", opts)
69-
}
70-
71-
// NewClientWithToken creates an Ironic client that sends X-Auth-Token on every request.
72-
func NewClientWithToken(ironicEndpoint, authToken string, opts ClientOptions) (*Client, error) {
73-
return newClient(ensureV1Suffix(ironicEndpoint), authToken, opts)
74-
}
75-
76-
func newClient(ironicEndpoint, authToken string, opts ClientOptions) (*Client, error) {
77-
client, err := noauth.NewBareMetalNoAuth(noauth.EndpointOpts{
78-
IronicEndpoint: ironicEndpoint,
79-
})
80-
if err != nil {
81-
return nil, fmt.Errorf("create ironic client: %w", err)
29+
type (
30+
Client struct {
31+
serviceClient *gophercloud.ServiceClient
8232
}
8333

84-
client.Microversion = microversion
85-
86-
baseTransport := http.DefaultTransport
87-
if opts.InsecureSkipVerify {
88-
if t, ok := baseTransport.(*http.Transport); ok {
89-
clone := t.Clone()
90-
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
91-
baseTransport = clone
92-
} else {
93-
baseTransport = &http.Transport{
94-
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
95-
}
96-
}
34+
TargetPowerState struct {
35+
powerstate nodes.TargetPowerState
9736
}
37+
)
9838

99-
transport := baseTransport
100-
if authToken != "" {
101-
transport = &tokenRoundTripper{rt: baseTransport, token: authToken}
102-
}
39+
var (
40+
PowerOn = TargetPowerState{nodes.PowerOn}
41+
PowerOff = TargetPowerState{nodes.PowerOff}
42+
Rebooting = TargetPowerState{nodes.Rebooting}
43+
SoftPowerOff = TargetPowerState{nodes.SoftPowerOff}
44+
SoftRebooting = TargetPowerState{nodes.SoftRebooting}
45+
)
10346

104-
if opts.HTTPClient != nil {
105-
client.HTTPClient = *opts.HTTPClient
106-
rt := opts.HTTPClient.Transport
107-
if rt == nil {
108-
rt = baseTransport
109-
} else if opts.InsecureSkipVerify {
110-
if t, ok := rt.(*http.Transport); ok {
111-
clone := t.Clone()
112-
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
113-
rt = clone
114-
}
115-
}
116-
if authToken != "" {
117-
rt = &tokenRoundTripper{rt: rt, token: authToken}
118-
}
119-
client.HTTPClient.Transport = rt
120-
} else {
121-
client.HTTPClient = http.Client{
122-
Transport: transport,
123-
Timeout: 60 * time.Second,
124-
}
47+
// NewClient creates an Ironic client with no-auth (typical for in-cluster Ironic with no Keystone).
48+
func NewClient() (*Client, error) {
49+
client, err := clientconfig.NewServiceClient(context.TODO(), "baremetal", nil)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to create baremetal client: %w", err)
12552
}
12653

127-
return &Client{serviceClient: client}, nil
54+
return &Client{
55+
serviceClient: client,
56+
}, nil
12857
}
12958

13059
// GetNode fetches a node by UUID or name from Ironic.
@@ -136,27 +65,15 @@ func (c *Client) GetNode(ctx context.Context, nodeID string) (*nodes.Node, error
13665
return node, nil
13766
}
13867

68+
func (c *Client) GetEndpoint() string {
69+
return c.serviceClient.Endpoint
70+
}
71+
13972
// SetPowerState requests power on or off for the node via Ironic.
140-
// target accepts "on"/"off" or Ironic values "power on"/"power off".
141-
func (c *Client) SetPowerState(ctx context.Context, nodeID, target string) error {
142-
t := normalizePowerTarget(target)
143-
if t == "" {
144-
return fmt.Errorf("invalid power target %q: use on, off, power on, or power off", target)
145-
}
146-
res := nodes.ChangePowerState(ctx, c.serviceClient, nodeID, nodes.PowerStateOpts{Target: t})
73+
func (c *Client) SetPowerState(ctx context.Context, nodeID string, target TargetPowerState) error {
74+
res := nodes.ChangePowerState(ctx, c.serviceClient, nodeID, nodes.PowerStateOpts{Target: target.powerstate})
14775
if err := res.ExtractErr(); err != nil {
148-
return fmt.Errorf("set power state on node %s: %w", nodeID, err)
76+
return fmt.Errorf("failed to set power state on node %s: %w", nodeID, err)
14977
}
15078
return nil
15179
}
152-
153-
func normalizePowerTarget(s string) nodes.TargetPowerState {
154-
switch strings.ToLower(strings.TrimSpace(s)) {
155-
case "on", "power on":
156-
return nodes.PowerOn
157-
case "off", "power off":
158-
return nodes.PowerOff
159-
default:
160-
return ""
161-
}
162-
}

0 commit comments

Comments
 (0)