* Copyright (c) 2026 Huawei Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain n copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
******************************************************************/
package releaseimage
import (
"context"
"fmt"
"sort"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
riv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/v1alpha1"
"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/oci"
"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/release/compatibility"
"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/release/imageref"
"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/release/manifest"
"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/log"
)
const releaseImageFinalizer = "releaseimage.config.openfuyao.com/finalizer"
const releaseImageControllerName = "releaseimage"
type ReleaseImageReconciler struct {
client.Client
Scheme *runtime.Scheme
Store *manifest.Store
Compatibility *compatibility.Engine
OCIClient *oci.Client
}
func (r *ReleaseImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.ControllerLogger(releaseImageControllerName).With("releaseImage", req.NamespacedName)
ri := &riv1beta1.ReleaseImage{}
if err := r.Get(ctx, req.NamespacedName, ri); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if !ri.DeletionTimestamp.IsZero() {
return r.reconcileDelete(ctx, logger, ri)
}
if !controllerutil.ContainsFinalizer(ri, releaseImageFinalizer) {
orig := ri.DeepCopy()
controllerutil.AddFinalizer(ri, releaseImageFinalizer)
if err := r.Patch(ctx, ri, client.MergeFrom(orig)); err != nil {
return ctrl.Result{}, fmt.Errorf("add release image finalizer: %w", err)
}
}
store := r.releaseStore()
engine := r.compatibilityEngine()
refs, err := r.buildReleaseRefs(ctx, ri)
if err != nil {
logger.Error(err, "release image repository resolve failed", "releaseImage", req.NamespacedName)
return ctrl.Result{}, r.updateStatus(ctx, ri, riv1beta1.ReleaseImagePhaseInvalid, "", err.Error(), nil)
}
var (
bundle *manifest.Bundle
files *manifest.BundleFiles
usedRef manifest.ReleaseRef
lastErr error
)
for _, ref := range refs {
bundle, files, err = store.RefreshRelease(ctx, ref)
if err == nil {
usedRef = ref
logger.Info("release image refreshed from OCI", "ociRef", ref.OCIRef)
break
}
lastErr = fmt.Errorf("refresh release from %s failed: %w", ref.OCIRef, err)
logger.Warn("release image refresh failed, trying next OCI ref", "ociRef", ref.OCIRef, "error", err.Error())
}
if bundle == nil {
msg := "release image refresh failed"
if lastErr != nil {
msg = lastErr.Error()
}
logger.Errorf("release image refresh failed: %v", lastErr)
return ctrl.Result{}, r.updateStatus(ctx, ri, riv1beta1.ReleaseImagePhaseInvalid, "", msg, nil)
}
report := engine.Check(ctx, bundle)
if !report.Allowed {
return ctrl.Result{}, r.updateStatus(ctx, ri, riv1beta1.ReleaseImagePhaseCompatibilityFailed,
bundle.Digest, report.Detail(), bundle)
}
if err := store.CommitRelease(usedRef, bundle, files); err != nil {
logger.Warn("commit validated release bundle to cache failed", "error", err.Error())
}
return ctrl.Result{}, r.updateStatus(ctx, ri, riv1beta1.ReleaseImagePhaseValid,
bundle.Digest, "release image validated", bundle)
}
func (r *ReleaseImageReconciler) reconcileDelete(
ctx context.Context,
logger interface {
Info(msg string, keysAndValues ...interface{})
Warn(msg string, keysAndValues ...interface{})
},
ri *riv1beta1.ReleaseImage,
) (ctrl.Result, error) {
if !controllerutil.ContainsFinalizer(ri, releaseImageFinalizer) {
return ctrl.Result{}, nil
}
store := r.releaseStore()
evicted := map[string]struct{}{}
for _, ref := range cacheRefsForReleaseImage(ri) {
key := ref.CacheKey()
if _, ok := evicted[key]; ok {
continue
}
if err := store.EvictRelease(ref); err != nil {
logger.Warn("evict release bundle cache failed", "cacheKey", key, "error", err.Error())
} else {
logger.Info("evicted release bundle cache", "cacheKey", key)
}
evicted[key] = struct{}{}
}
orig := ri.DeepCopy()
controllerutil.RemoveFinalizer(ri, releaseImageFinalizer)
if err := r.Patch(ctx, ri, client.MergeFrom(orig)); err != nil {
return ctrl.Result{}, fmt.Errorf("remove release image finalizer: %w", err)
}
return ctrl.Result{}, nil
}
func (r *ReleaseImageReconciler) buildReleaseRefs(
ctx context.Context,
ri *riv1beta1.ReleaseImage,
) ([]manifest.ReleaseRef, error) {
ociRefs, err := imageref.ReleaseImageRefs(
ctx,
r.Client,
ri.Namespace,
ri.Annotations[imageref.AnnotationBKEClusterName],
ri.Spec.Version,
)
if err != nil {
return nil, err
}
refs := make([]manifest.ReleaseRef, 0, len(ociRefs))
for _, ociRef := range ociRefs {
refs = append(refs, releaseRefFromSpec(ri, ociRef))
}
if len(refs) == 0 {
return nil, fmt.Errorf("no release OCI refs resolved for version %q", ri.Spec.Version)
}
return refs, nil
}
func releaseRefFromSpec(ri *riv1beta1.ReleaseImage, ociRef string) manifest.ReleaseRef {
return manifest.ReleaseRef{
Version: ri.Spec.Version,
OCIRef: ociRef,
Digest: ri.Spec.Digest,
VerifySignature: ri.Spec.VerifySignature,
SignatureKey: ri.Spec.SignatureKey,
AllowCacheFallback: ri.Spec.AllowCacheFallback,
}
}
func cacheRefsForReleaseImage(ri *riv1beta1.ReleaseImage) []manifest.ReleaseRef {
if ri == nil {
return nil
}
return []manifest.ReleaseRef{{
Version: ri.Spec.Version,
Digest: strings.TrimSpace(ri.Spec.Digest),
VerifySignature: ri.Spec.VerifySignature,
SignatureKey: ri.Spec.SignatureKey,
AllowCacheFallback: ri.Spec.AllowCacheFallback,
}}
}
func (r *ReleaseImageReconciler) releaseStore() *manifest.Store {
if r.Store != nil {
return r.Store
}
return manifest.NewStore(manifest.ReleaseCacheDir(), manifest.OCIPuller{Client: r.OCIClient}, nil)
}
func (r *ReleaseImageReconciler) compatibilityEngine() *compatibility.Engine {
if r.Compatibility != nil {
return r.Compatibility
}
return compatibility.NewEngine()
}
func (r *ReleaseImageReconciler) updateStatus(ctx context.Context, ri *riv1beta1.ReleaseImage,
phase riv1beta1.ReleaseImagePhase, digest, message string, bundle *manifest.Bundle) error {
orig := ri.DeepCopy()
ri.Status.Phase = phase
ri.Status.Digest = digest
ri.Status.Message = message
ri.Status.CompatibilityReport = message
ri.Status.ComponentCount = 0
ri.Status.Components = nil
ri.Status.Source = ""
ri.Status.CacheFallback = false
if bundle != nil {
ri.Status.ComponentCount = len(bundle.Components)
ri.Status.Components = componentStatuses(bundle)
ri.Status.Source = bundle.Source
ri.Status.CacheFallback = bundle.CacheFallback
}
now := metav1.Now()
ri.Status.ValidatedAt = &now
if err := r.Status().Patch(ctx, ri, client.MergeFrom(orig)); err != nil {
return fmt.Errorf("update release image status: %w", err)
}
return nil
}
func componentStatuses(bundle *manifest.Bundle) []riv1beta1.ComponentStatus {
if bundle == nil || len(bundle.Components) == 0 {
return nil
}
components := make([]riv1beta1.ComponentStatus, 0, len(bundle.Components))
for _, component := range bundle.Components {
components = append(components, riv1beta1.ComponentStatus{
Name: component.Spec.Name,
Version: component.Spec.Version,
Type: component.Spec.Type,
})
}
sort.Slice(components, func(i, j int) bool {
if components[i].Name == components[j].Name {
return components[i].Version < components[j].Version
}
return components[i].Name < components[j].Name
})
return components
}
func (r *ReleaseImageReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&riv1beta1.ReleaseImage{}).
Named("releaseimage").
Complete(r)
}