Skip to content

Commit 623a641

Browse files
committed
Migrate to go-plugin system for provisioners
Signed-off-by: s3rj1k <evasive.gyron@gmail.com>
1 parent c202b9a commit 623a641

8 files changed

Lines changed: 330 additions & 25 deletions

File tree

Dockerfile

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
11
# Support FROM override
22
ARG BUILD_IMAGE=docker.io/golang:1.25.9@sha256:5ab234a9519e05043f4a97a505a59f21dc40eee172d6b17d411863d6bba599bb
3-
ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f
3+
ARG BASE_IMAGE=gcr.io/distroless/base-debian13:nonroot@sha256:fb282f8ed3057f71dbfe3ea0f5fa7e961415dafe4761c23948a9d4628c6166fe
44

5-
# Build the manager binary
6-
FROM $BUILD_IMAGE AS builder
5+
# Shared SDK stage: pinned Go toolchain, modules, and checked-out tree.
6+
# Downstream builder stages copy from here so we pay the `go mod download`
7+
# cost once. Third parties can also build custom provisioner plugins against
8+
# this image to guarantee toolchain and module-version parity with BMO.
9+
FROM $BUILD_IMAGE AS sdk
710

811
WORKDIR /workspace
912

10-
# Bring in the go dependencies before anything else so we can take
11-
# advantage of caching these layers in future builds.
1213
COPY go.mod go.sum ./
1314
COPY apis/go.mod apis/go.sum apis/
1415
COPY hack/tools/go.mod hack/tools/go.sum hack/tools/
1516
COPY pkg/hardwareutils/go.mod pkg/hardwareutils/go.sum pkg/hardwareutils/
1617
RUN go mod download
17-
ARG LDFLAGS=-s -w -extldflags=-static
1818

1919
COPY . .
20+
21+
ENV CGO_ENABLED=1
22+
ENV GO111MODULE=on
23+
24+
# Build the manager binary
25+
FROM sdk AS builder
26+
ARG ARCH=amd64
27+
ARG LDFLAGS=-s -w
28+
RUN CGO_ENABLED=1 GOOS=linux GOARCH=${ARCH} GO111MODULE=on \
29+
go build -a -ldflags "${LDFLAGS}" -o baremetal-operator main.go
30+
31+
# Build the ironic provisioner plugin
32+
FROM sdk AS ironic-plugin-builder
33+
ARG ARCH=amd64
34+
ARG LDFLAGS=-s -w
35+
RUN GOOS=linux GOARCH=${ARCH} \
36+
go build -buildmode=plugin -ldflags "${LDFLAGS}" \
37+
-o ironic-provisioner.so ./pkg/provisioner/ironic/plugin/
38+
39+
# Build the demo provisioner plugin
40+
FROM sdk AS demo-plugin-builder
2041
ARG ARCH=amd64
21-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} GO111MODULE=on go build -a -ldflags "${LDFLAGS}" -o baremetal-operator main.go
42+
ARG LDFLAGS=-s -w
43+
RUN GOOS=linux GOARCH=${ARCH} \
44+
go build -buildmode=plugin -ldflags "${LDFLAGS}" \
45+
-o demo-provisioner.so ./pkg/provisioner/demo/plugin/
2246

23-
# Copy the controller-manager into a thin image
24-
# BMO has a dependency preventing us to use the static one,
25-
# using the base one instead
47+
# Runtime image. Uses distroless/base (not static) because Go plugins need glibc.
2648
FROM $BASE_IMAGE
2749

2850
# image.version is set during image build by automation
@@ -36,5 +58,7 @@ LABEL org.opencontainers.image.vendor="Metal3-io"
3658

3759
WORKDIR /
3860
COPY --from=builder /workspace/baremetal-operator .
61+
COPY --from=ironic-plugin-builder /workspace/ironic-provisioner.so /plugins/ironic-provisioner.so
62+
COPY --from=demo-plugin-builder /workspace/demo-provisioner.so /plugins/demo-provisioner.so
3963
USER nonroot:nonroot
4064
ENTRYPOINT ["/baremetal-operator"]

Makefile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,32 @@ docker-debug: generate manifests ## Build the docker image with debug info
321321
--build-arg LDFLAGS="-extldflags=-static" \
322322
. -t ${IMG}-$(ARCH):${IMG_TAG}
323323

324+
## --------------------------------------
325+
## Plugin / SDK Targets
326+
## --------------------------------------
327+
328+
IRONIC_PLUGIN_DIR = pkg/provisioner/ironic/plugin
329+
IRONIC_PLUGIN_SO = bin/ironic-provisioner.so
330+
331+
.PHONY: ironic-plugin
332+
ironic-plugin: ## Build the ironic provisioner plugin .so locally
333+
CGO_ENABLED=1 go build -buildmode=plugin -ldflags "$(LDFLAGS)" -o $(IRONIC_PLUGIN_SO) ./$(IRONIC_PLUGIN_DIR)/
334+
335+
.PHONY: docker-build-sdk
336+
docker-build-sdk: ## Build the BMO SDK image for authoring custom provisioner plugins
337+
$(CONTAINER_RUNTIME) build --platform=linux/$(ARCH) \
338+
--build-arg ARCH=$(ARCH) \
339+
--build-arg http_proxy=$(http_proxy) \
340+
--build-arg https_proxy=$(https_proxy) \
341+
--target sdk \
342+
. -t ${IMG}-sdk-$(ARCH):${IMG_TAG}
343+
344+
.PHONY: docker-build-sdk-all
345+
docker-build-sdk-all: $(addprefix docker-build-sdk-,$(ALL_ARCH)) ## Build the SDK image for all architectures
346+
347+
docker-build-sdk-%:
348+
$(MAKE) ARCH=$* docker-build-sdk
349+
324350
## --------------------------------------
325351
## Docker — All ARCH
326352
## --------------------------------------

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ components, see the [Metal³ docs](https://github.com/metal3-io/metal3-docs).
3232
- [Setup Development Environment](docs/dev-setup.md)
3333
- [Configuration](docs/configuration.md)
3434
- [Testing](docs/testing.md)
35+
- [Provisioner plugins](docs/plugin-provisioners.md)
3536

3637
## Integration tests
3738

docs/plugin-provisioners.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Provisioner plugins
2+
3+
BMO loads its provisioner at runtime from a Go plugin `.so`. The manager binary
4+
is decoupled from provisioner-specific dependencies (e.g. the Ironic client).
5+
6+
## What ships in the BMO image
7+
8+
The container image bakes two plugins under `/plugins/`:
9+
10+
| Path | Provisioner |
11+
|--------------------------------------|------------------|
12+
| `/plugins/ironic-provisioner.so` | ironic (default) |
13+
| `/plugins/demo-provisioner.so` | demo |
14+
15+
The `fixture` provisioner is compiled into the manager and selected with
16+
`--test-mode` for tests and offline runs.
17+
18+
## Selecting a plugin
19+
20+
```text
21+
--provisioner-plugin=/path/to/custom.so
22+
```
23+
24+
Also reads `$PROVISIONER_PLUGIN`. Defaults to the ironic path when unset.
25+
26+
## Writing a custom plugin
27+
28+
A plugin is `package main` exporting two symbols: `PluginName() string` and
29+
`NewProvisionerFactory(provisioner.PluginConfig) (provisioner.Factory, error)`.
30+
See [`pkg/provisioner/demo/plugin/main.go`](../pkg/provisioner/demo/plugin/main.go)
31+
for a minimal reference.
32+
33+
Build:
34+
35+
```sh
36+
CGO_ENABLED=1 go build -buildmode=plugin -o myprov.so ./path/to/plugin
37+
```
38+
39+
## Toolchain lock
40+
41+
Plugins must be built with the **same Go toolchain and same build flags** as
42+
the BMO binary they load into, and every module shared with the host (directly
43+
or transitively, including stdlib) must resolve to the **exact same version**.
44+
Deps unique to the plugin can be added freely because they don't participate
45+
in the version check. Mismatches produce:
46+
47+
```text
48+
plugin was built with a different version of package <pkg>
49+
```
50+
51+
For an in-tree plugin you get this for free, since the root `go.mod` is shared.
52+
53+
For an out-of-tree plugin maintained in its own repo:
54+
55+
- Pin BMO in the plugin's `go.mod` at the release you target. Go's module
56+
resolution will then pick versions of shared deps (`k8s.io/*`,
57+
`controller-runtime`, `go-logr`, etc.) compatible with BMO's closure:
58+
59+
```sh
60+
go get github.com/metal3-io/baremetal-operator@vX.Y.Z
61+
go mod tidy
62+
```
63+
64+
- Build with the same Go toolchain (see `go-version` target in BMO's
65+
Makefile):
66+
67+
```sh
68+
CGO_ENABLED=1 go build -buildmode=plugin -o myprov.so ./plugin
69+
```
70+
71+
- If `plugin.Open` reports a mismatch on a specific package, align it in
72+
your `go.mod` (`replace` or `go get <pkg>@<ver>`) to the version used by
73+
the BMO release, then rebuild.

main.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616
package main
1717

1818
import (
19+
"cmp"
1920
"crypto/tls"
2021
"flag"
2122
"fmt"
@@ -30,9 +31,7 @@ import (
3031
webhooks "github.com/metal3-io/baremetal-operator/internal/webhooks/metal3.io/v1alpha1"
3132
"github.com/metal3-io/baremetal-operator/pkg/imageprovider"
3233
"github.com/metal3-io/baremetal-operator/pkg/provisioner"
33-
"github.com/metal3-io/baremetal-operator/pkg/provisioner/demo"
3434
"github.com/metal3-io/baremetal-operator/pkg/provisioner/fixture"
35-
"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic"
3635
"github.com/metal3-io/baremetal-operator/pkg/secretutils"
3736
"github.com/metal3-io/baremetal-operator/pkg/version"
3837
ironicv1alpha1 "github.com/metal3-io/ironic-standalone-operator/api/v1alpha1"
@@ -73,6 +72,9 @@ var (
7372

7473
const leaderElectionID = "baremetal-operator"
7574

75+
// defaultIronicPluginPath is where the BMO image bakes the ironic provisioner plugin.
76+
const defaultIronicPluginPath = "/plugins/ironic-provisioner.so"
77+
7678
func init() {
7779
_ = clientgoscheme.AddToScheme(scheme)
7880

@@ -132,7 +134,7 @@ func main() {
132134
var preprovImgEnable bool
133135
var devLogging bool
134136
var runInTestMode bool
135-
var runInDemoMode bool
137+
var provisionerPlugin string
136138
var webhookPort int
137139
var restConfigQPS float64
138140
var restConfigBurst int
@@ -155,8 +157,8 @@ func main() {
155157
flag.BoolVar(&preprovImgEnable, "build-preprov-image", false, "enable integration with the PreprovisioningImage API")
156158
flag.BoolVar(&devLogging, "dev", false, "enable developer logging")
157159
flag.BoolVar(&runInTestMode, "test-mode", false, "disable ironic communication")
158-
flag.BoolVar(&runInDemoMode, "demo-mode", false,
159-
"use the demo provisioner to set host states")
160+
flag.StringVar(&provisionerPlugin, "provisioner-plugin", os.Getenv("PROVISIONER_PLUGIN"),
161+
"Path to a Go plugin .so implementing the provisioner Factory. Defaults to "+defaultIronicPluginPath+".")
160162
flag.StringVar(&healthAddr, "health-addr", ":9440",
161163
"The address the health endpoint binds to.")
162164
flag.IntVar(&webhookPort, "webhook-port", 9443, //nolint:mnd
@@ -317,22 +319,25 @@ func main() {
317319
if runInTestMode {
318320
ctrl.Log.Info("using test provisioner")
319321
provisionerFactory = &fixture.Fixture{}
320-
} else if runInDemoMode {
321-
ctrl.Log.Info("using demo provisioner")
322-
provisionerFactory = &demo.Demo{}
323322
} else {
323+
pluginPath := cmp.Or(provisionerPlugin, defaultIronicPluginPath)
324324
provLog := zap.New(zap.UseFlagOptions(&logOpts)).WithName("provisioner")
325-
// Check if we should use Ironic CR integration
326-
if ironicName != "" && ironicNamespace != "" {
327-
provisionerFactory, err = ironic.NewProvisionerFactoryWithClient(provLog, preprovImgEnable,
328-
mgr.GetClient(), mgr.GetAPIReader(), ironicName, ironicNamespace)
329-
} else {
330-
provisionerFactory, err = ironic.NewProvisionerFactory(provLog, preprovImgEnable)
325+
var hostFeatures []provisioner.HostFeature
326+
if preprovImgEnable {
327+
hostFeatures = append(hostFeatures, provisioner.FeaturePreprovImg)
331328
}
329+
var pluginName string
330+
provisionerFactory, pluginName, err = provisioner.LoadProvisionerPlugin(pluginPath, provisioner.PluginConfig{
331+
Logger: provLog,
332+
Features: hostFeatures,
333+
K8sClient: mgr.GetClient(),
334+
APIReader: mgr.GetAPIReader(),
335+
})
332336
if err != nil {
333-
setupLog.Error(err, "cannot start ironic provisioner")
337+
setupLog.Error(err, "cannot load provisioner plugin", "path", pluginPath)
334338
os.Exit(1)
335339
}
340+
ctrl.Log.Info("loaded provisioner plugin", "name", pluginName, "path", pluginPath)
336341
}
337342

338343
maxConcurrency, err := getMaxConcurrentReconciles(controllerConcurrency)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package main
17+
18+
import (
19+
"github.com/metal3-io/baremetal-operator/pkg/provisioner"
20+
"github.com/metal3-io/baremetal-operator/pkg/provisioner/demo"
21+
)
22+
23+
// PluginName is advertised to the host via plugin.Lookup.
24+
//
25+
//nolint:unused // resolved by plugin.Lookup; see pkg/provisioner/plugin.go
26+
func PluginName() string { return "demo" }
27+
28+
// NewProvisionerFactory is the exported symbol that BMO looks up in the plugin.
29+
// It is resolved at runtime via plugin.Lookup from the host binary, so static
30+
// analysis cannot see the reference.
31+
//
32+
//nolint:unused // resolved by plugin.Lookup; see pkg/provisioner/plugin.go
33+
func NewProvisionerFactory(_ provisioner.PluginConfig) (provisioner.Factory, error) {
34+
return &demo.Demo{}, nil
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package main
17+
18+
import (
19+
"os"
20+
21+
"github.com/metal3-io/baremetal-operator/pkg/provisioner"
22+
"github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic"
23+
)
24+
25+
const pluginName = "ironic"
26+
27+
// PluginName is advertised to the host via plugin.Lookup.
28+
//
29+
//nolint:unused // resolved by plugin.Lookup; see pkg/provisioner/plugin.go
30+
func PluginName() string { return pluginName }
31+
32+
// NewProvisionerFactory is the exported symbol that BMO looks up in the plugin.
33+
// It is resolved at runtime via plugin.Lookup from the host binary, so static
34+
// analysis cannot see the reference.
35+
//
36+
//nolint:unused // resolved by plugin.Lookup; see pkg/provisioner/plugin.go
37+
func NewProvisionerFactory(config provisioner.PluginConfig) (provisioner.Factory, error) {
38+
logger := config.Logger.WithName(pluginName)
39+
40+
ironicName := os.Getenv("IRONIC_NAME")
41+
ironicNamespace := os.Getenv("IRONIC_NAMESPACE")
42+
43+
havePreprovImgBuilder := config.HasFeature(provisioner.FeaturePreprovImg)
44+
45+
if config.K8sClient != nil && ironicName != "" && ironicNamespace != "" {
46+
return ironic.NewProvisionerFactoryWithClient(
47+
logger, havePreprovImgBuilder,
48+
config.K8sClient, config.APIReader,
49+
ironicName, ironicNamespace,
50+
)
51+
}
52+
53+
return ironic.NewProvisionerFactory(logger, havePreprovImgBuilder)
54+
}

0 commit comments

Comments
 (0)