Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ junit/
# Buildchain artifacts
/_build/

vagrant_config.rb
vagrant_config.rb

# Binaries downloaded by the upgrade-operator-sdk.py script
/.tmp/
102 changes: 92 additions & 10 deletions BUMPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,98 @@ A few tips to bump image versions and SHAs:

This guide is applied for both `metalk8s-operator` and `storage-operator`.

- check [documentation](https://sdk.operatorframework.io/docs/upgrading-sdk-version/$version)
for important changes and apply them.
- bump version in Makefile.
- if necessary, bump go version in pre_merge github action.
- if necessary, bump go version in Dockerfile.
- if necessary, bump go dependencies versions.
- in the root of each operator, run `go mod tidy`.
- run `make metalk8s`
- check a diff between the two latest versions of this [test project](https://github.com/operator-framework/operator-sdk/tree/master/testdata/go/v4/memcached-operator)
- the diff in this repo and the test project should be more or less the same
### Prerequisites

- `go`, `curl`, and `patch` in `PATH`.
- `pyyaml` Python package: `pip install pyyaml`
- `GITHUB_TOKEN` (optional): raises the GitHub API rate limit from 60 to 5000
req/hour. Set via `export GITHUB_TOKEN=<token>`.

### Updating the versions

Target versions are pinned in `scripts/upgrade-operator-sdk/<name>/config.yaml`:

```yaml
operator_sdk_version: v1.42.1 # target operator-sdk release
go_toolchain: go1.24.13 # pin Go toolchain (for GOTOOLCHAIN)
k8s_libs: v0.33.10 # pin k8s.io libs version
```

After scaffolding, the script detects the latest available versions (operator-sdk
from GitHub, Go and k8s.io patches from go.dev / module proxy) and compares with
the pinned values:

- **No pin** in YAML: the detected version is used and auto-pinned in the file.
- **Pin matches detected**: all good, no action.
- **Pin is older** than detected: warning printed with the newer version available.
The pinned value is still used. Update the YAML manually when ready.
- **Pin is newer** than detected (unusual): warning, the detected value is used.

This is CI-friendly: zero interactive input during reconciliation.

### Running the upgrade

The script processes one operator at a time:

```bash
python3 scripts/upgrade-operator-sdk/upgrade.py \
--operator-dir operator \
scripts/upgrade-operator-sdk/operator

python3 scripts/upgrade-operator-sdk/upgrade.py \
--operator-dir storage-operator \
scripts/upgrade-operator-sdk/storage-operator
```

Options:

```
--operator-dir Path to the operator project directory (required)
--skip-backup Reuse an existing .bak directory (no new backup)
--clean-tools Remove tool cache after upgrade
--yes, -y Skip the confirmation prompt
```

### YAML config files

Each operator has a config directory at `scripts/upgrade-operator-sdk/<name>/` containing
`config.yaml` and a `patches/` subdirectory. The config fields are:

- **Versions**: `operator_sdk_version`, `go_toolchain` (optional pin), `k8s_libs` (optional pin)
- **Scaffold**: `repo`, `domain`, `apis` (with `group`, `version`, `kind`, `namespaced`). The operator name is derived from the config directory name.
- **Raw copy**: `raw_copy` -- directories or files copied as-is from backup (purely custom code with no scaffold equivalent: `pkg/`, `version/`, `config/metalk8s/`, `salt/`, individual test/helper files)
- **Post-processing**: `image_placeholder`, `extra_commands`

### Patch files

All customizations to scaffold-generated files are stored as GNU unified diff
files in the `patches/` subdirectory. This includes:

- **Dockerfile** and **Makefile** customizations
- **CRD type definitions** (`*_types.go`)
- **Controller implementations** (`*_controller.go`)
- **Scaffold test stubs** (`*_controller_test.go`) -- neutralized when incompatible with the delegation pattern

The script applies them with `patch -p1` after scaffolding. If a patch does not
apply cleanly, look for `.rej` files and resolve manually.

Patch files use `__PLACEHOLDER__` tokens for runtime values:

| Placeholder | Replaced with | Source |
| ----------------- | ------------------------------- | ---------- |
| `__GOTOOLCHAIN__` | Detected/pinned Go toolchain | `Makefile` |
| `__IMAGE__` | `image_placeholder` from config | `Makefile` |

New `.patch` files in the patches directory are automatically picked up.

### What to review after the upgrade

1. `git diff` to review all changes
2. `cd <operator> && make test` to run tests
3. Check `config/crd/bases/` for correct CRD scopes
4. Check `config/rbac/role.yaml` for RBAC completeness
5. Check `deploy/manifests.yaml` for correct Jinja templates
6. Remove backup: `rm -rf <operator>.bak/`

## Calico

Expand Down
33 changes: 33 additions & 0 deletions tools/upgrade-operator-sdk/operator/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
repo: github.com/scality/metalk8s/operator
domain: metalk8s.scality.com

operator_sdk_version: v1.42.1

# Optional: pin versions. If absent, the script detects the latest
# patch from the scaffold's go.mod and auto-pins them here.
go_toolchain: go1.24.13
k8s_libs: v0.33.10

apis:
- version: v1alpha1
kind: ClusterConfig
namespaced: false
- version: v1alpha1
kind: VirtualIPPool
namespaced: true

# Directories/files copied as-is from backup (purely custom, no scaffold equivalent).
raw_copy:
- pkg
- version
- config/metalk8s
- api/v1alpha1/conditions.go
- api/v1alpha1/conditions_test.go
- api/v1alpha1/clusterconfig_types_test.go
- api/v1alpha1/virtualippool_types_test.go
- api/v1alpha1/v1alpha1_suite_test.go

image_placeholder: '{{ build_image_name("metalk8s-operator") }}'

extra_commands:
- ["make", "metalk8s"]
65 changes: 65 additions & 0 deletions tools/upgrade-operator-sdk/operator/patches/Dockerfile.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,13 +15,20 @@
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/ internal/
+COPY pkg/ pkg/
+COPY version/ version/
+
+# Version of the project, e.g. `git describe --always --long --dirty --broken`
+ARG METALK8S_VERSION

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
-RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
+RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager \
+ -ldflags "-X 'github.com/scality/metalk8s/operator/version.Version=${METALK8S_VERSION}'" \
+ cmd/main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
@@ -31,3 +38,40 @@
USER 65532:65532

ENTRYPOINT ["/manager"]
+
+# Timestamp of the build, formatted as RFC3339
+ARG BUILD_DATE
+# Git revision o the tree at build time
+ARG VCS_REF
+# Version of the image
+ARG VERSION
+# Version of the project, e.g. `git describe --always --long --dirty --broken`
+ARG METALK8S_VERSION
+
+# These contain BUILD_DATE so should come 'late' for layer caching
+LABEL maintainer="squad-metalk8s@scality.com" \
+ # http://label-schema.org/rc1/
+ org.label-schema.build-date="$BUILD_DATE" \
+ org.label-schema.name="metalk8s-operator" \
+ org.label-schema.description="Kubernetes Operator for managing MetalK8s cluster config" \
+ org.label-schema.url="https://github.com/scality/metalk8s/" \
+ org.label-schema.vcs-url="https://github.com/scality/metalk8s.git" \
+ org.label-schema.vcs-ref="$VCS_REF" \
+ org.label-schema.vendor="Scality" \
+ org.label-schema.version="$VERSION" \
+ org.label-schema.schema-version="1.0" \
+ # https://github.com/opencontainers/image-spec/blob/master/annotations.md
+ org.opencontainers.image.created="$BUILD_DATE" \
+ org.opencontainers.image.authors="squad-metalk8s@scality.com" \
+ org.opencontainers.image.url="https://github.com/scality/metalk8s/" \
+ org.opencontainers.image.source="https://github.com/scality/metalk8s.git" \
+ org.opencontainers.image.version="$VERSION" \
+ org.opencontainers.image.revision="$VCS_REF" \
+ org.opencontainers.image.vendor="Scality" \
+ org.opencontainers.image.title="metalk8s-operator" \
+ org.opencontainers.image.description="Kubernetes Operator for managing MetalK8s cluster config" \
+ # https://docs.openshift.org/latest/creating_images/metadata.html
+ io.openshift.tags="metalk8s,operator" \
+ io.k8s.description="Kubernetes Operator for managing MetalK8s cluster config" \
+ # Various
+ com.scality.metalk8s.version="$METALK8S_VERSION"
15 changes: 15 additions & 0 deletions tools/upgrade-operator-sdk/operator/patches/Makefile.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--- a/Makefile
+++ b/Makefile
@@ -3,3 +3,12 @@
.PHONY: catalog-push
catalog-push: ## Push a catalog image.
$(MAKE) docker-push IMG=$(CATALOG_IMG)
+
+# Force Go toolchain version
+export GOTOOLCHAIN = __GOTOOLCHAIN__
+
+.PHONY: metalk8s
+metalk8s: manifests kustomize ## Generate MetalK8s resulting manifests
+ mkdir -p deploy
+ $(KUSTOMIZE) build config/metalk8s | \
+ sed 's/BUILD_IMAGE_CLUSTER_OPERATOR:latest/__IMAGE__/' > deploy/manifests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
--- a/internal/controller/clusterconfig_controller.go
+++ b/internal/controller/clusterconfig_controller.go
@@ -1,5 +1,5 @@
/*
-Copyright 2026.
+Copyright 2022.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,14 +17,10 @@
package controller

import (
- "context"
-
+ "github.com/scality/metalk8s/operator/pkg/controller/clusterconfig"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
-
- metalk8sscalitycomv1alpha1 "github.com/scality/metalk8s/operator/api/v1alpha1"
)

// ClusterConfigReconciler reconciles a ClusterConfig object
@@ -33,9 +29,13 @@
Scheme *runtime.Scheme
}

-// +kubebuilder:rbac:groups=metalk8s.scality.com,resources=clusterconfigs,verbs=get;list;watch;create;update;patch;delete
-// +kubebuilder:rbac:groups=metalk8s.scality.com,resources=clusterconfigs/status,verbs=get;update;patch
-// +kubebuilder:rbac:groups=metalk8s.scality.com,resources=clusterconfigs/finalizers,verbs=update
+//+kubebuilder:rbac:groups=metalk8s.scality.com,resources=clusterconfigs,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=metalk8s.scality.com,resources=clusterconfigs/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=metalk8s.scality.com,resources=clusterconfigs/finalizers,verbs=update
+
+//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
+//+kubebuilder:rbac:groups=metalk8s.scality.com,resources=virtualippools,verbs=get;list;watch;create;update;patch;delete

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
@@ -45,19 +45,18 @@
// the user.
//
// For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
-func (r *ClusterConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
- _ = logf.FromContext(ctx)
-
- // TODO(user): your logic here
-
- return ctrl.Result{}, nil
-}
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile
+//func (r *ClusterConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+// _ = log.FromContext(ctx)
+//
+// return ctrl.Result{}, nil
+//}

// SetupWithManager sets up the controller with the Manager.
func (r *ClusterConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
- return ctrl.NewControllerManagedBy(mgr).
- For(&metalk8sscalitycomv1alpha1.ClusterConfig{}).
- Named("clusterconfig").
- Complete(r)
+ return clusterconfig.Add(mgr)
+
+ //return ctrl.NewControllerManagedBy(mgr).
+ // For(&metalk8sscalitycomv1alpha1.ClusterConfig{}).
+ // Complete(r)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
--- a/internal/controller/clusterconfig_controller_test.go
+++ b/internal/controller/clusterconfig_controller_test.go
@@ -1,84 +1 @@
-/*
-Copyright 2026.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
package controller
-
-import (
- "context"
-
- . "github.com/onsi/ginkgo/v2"
- . "github.com/onsi/gomega"
- "k8s.io/apimachinery/pkg/api/errors"
- "k8s.io/apimachinery/pkg/types"
- "sigs.k8s.io/controller-runtime/pkg/reconcile"
-
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
- metalk8sscalitycomv1alpha1 "github.com/scality/metalk8s/operator/api/v1alpha1"
-)
-
-var _ = Describe("ClusterConfig Controller", func() {
- Context("When reconciling a resource", func() {
- const resourceName = "test-resource"
-
- ctx := context.Background()
-
- typeNamespacedName := types.NamespacedName{
- Name: resourceName,
- Namespace: "default", // TODO(user):Modify as needed
- }
- clusterconfig := &metalk8sscalitycomv1alpha1.ClusterConfig{}
-
- BeforeEach(func() {
- By("creating the custom resource for the Kind ClusterConfig")
- err := k8sClient.Get(ctx, typeNamespacedName, clusterconfig)
- if err != nil && errors.IsNotFound(err) {
- resource := &metalk8sscalitycomv1alpha1.ClusterConfig{
- ObjectMeta: metav1.ObjectMeta{
- Name: resourceName,
- Namespace: "default",
- },
- // TODO(user): Specify other spec details if needed.
- }
- Expect(k8sClient.Create(ctx, resource)).To(Succeed())
- }
- })
-
- AfterEach(func() {
- // TODO(user): Cleanup logic after each test, like removing the resource instance.
- resource := &metalk8sscalitycomv1alpha1.ClusterConfig{}
- err := k8sClient.Get(ctx, typeNamespacedName, resource)
- Expect(err).NotTo(HaveOccurred())
-
- By("Cleanup the specific resource instance ClusterConfig")
- Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
- })
- It("should successfully reconcile the resource", func() {
- By("Reconciling the created resource")
- controllerReconciler := &ClusterConfigReconciler{
- Client: k8sClient,
- Scheme: k8sClient.Scheme(),
- }
-
- _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
- NamespacedName: typeNamespacedName,
- })
- Expect(err).NotTo(HaveOccurred())
- // TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
- // Example: If you expect a certain status condition after reconciliation, verify it here.
- })
- })
-})
Loading
Loading