* Copyright (c) 2025 Huawei Technologies Co., Ltd.
* openFuyao 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 a 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 installer
import (
"context"
"fmt"
"slices"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/kubernetes"
"installer-service/pkg/constant"
"installer-service/pkg/utils/k8sutil"
"installer-service/pkg/zlog"
)
const (
MinSemverSegments = 3
PatchVersionIndex = 2
)
func getPatchInfo(c *installerClient, obj *unstructured.Unstructured) (string, error) {
openFuyaoVersion, err := getOpenFuyaoVersionFromUnstructured(obj)
zlog.Infoln("openFuyaoVersion in unstructured is", openFuyaoVersion)
if err != nil {
return "", errors.Wrap(err, "failed to get openFuyao version from input")
}
versionList, err := c.GetUpgradeVersions()
if err != nil {
return "", errors.Wrap(err, "failed to get available openFuyao versions")
}
versionFound := slices.Contains(versionList, openFuyaoVersion)
if !versionFound {
return "", fmt.Errorf("openFuyaoVersion '%s' is not supported. Available versions: %v",
openFuyaoVersion, versionList)
}
configMap, err := k8sutil.GetConfigMap(c.clientset, BKEConfigCmKey().Name, BKEConfigCmKey().Namespace)
if err != nil {
return "", errors.Wrapf(err, "failed to get ConfigMap %s/%s",
BKEConfigCmKey().Namespace, BKEConfigCmKey().Name)
}
patchConfigMapKey := constant.PatchKeyPrefix + openFuyaoVersion
patchConfigMapName, exists := configMap.Data[patchConfigMapKey]
if !exists {
return "", fmt.Errorf("patch file path not found in ConfigMap for key '%s'", patchConfigMapKey)
}
if strings.TrimSpace(patchConfigMapName) == "" {
return "", fmt.Errorf("patch config map is empty for version '%s'", openFuyaoVersion)
}
return patchConfigMapName, nil
}
func updateCRDAddonsWithPatch(addonsRaw []interface{}, patchAddons []AddonInfo) {
patchMap := buildPatchMap(patchAddons)
for i, raw := range addonsRaw {
addon, ok := raw.(map[string]interface{})
if !ok {
continue
}
name, ok := getString(addon, "name")
if !ok || name == "" {
continue
}
patchAddon, exists := patchMap[name]
if !exists {
continue
}
applyPatchToAddon(addon, patchAddon)
addonsRaw[i] = addon
}
}
func buildPatchMap(addons []AddonInfo) map[string]AddonInfo {
m := make(map[string]AddonInfo, len(addons))
for _, a := range addons {
m[a.Name] = a
}
return m
}
func getString(m map[string]interface{}, key string) (string, bool) {
v, ok := m[key]
if !ok {
return "", false
}
s, ok := v.(string)
return s, ok
}
func applyPatchToAddon(addon map[string]interface{}, patch AddonInfo) {
if addon == nil {
return
}
if patch.Version != "" {
addon["version"] = patch.Version
}
addon["block"] = patch.Block
if len(patch.Param) == 0 {
return
}
currentParam := getOrCreateParamMap(addon)
for k, v := range patch.Param {
currentParam[k] = v
}
addon["param"] = currentParam
}
func getOrCreateParamMap(addon map[string]interface{}) map[string]interface{} {
if raw, ok := addon["param"]; ok {
if m, ok := raw.(map[string]interface{}); ok {
return m
}
if oldMap, ok := raw.(map[string]string); ok {
newMap := make(map[string]interface{}, len(oldMap))
for k, v := range oldMap {
newMap[k] = v
}
return newMap
}
}
return make(map[string]interface{})
}
func patchAddonsInfo(c *installerClient, obj *unstructured.Unstructured) error {
patchConfigMapName, err := getPatchInfo(c, obj)
if err != nil {
return errors.Wrap(err, "get patch config map info failed")
}
patchAddons, err := getAddonsFromPatchCM(c.clientset, patchConfigMapName)
if err != nil {
return errors.Wrap(err, "get addons for config map failed")
}
zlog.Infoln("addon is", patchAddons)
addonsRaw, found, err := unstructured.NestedSlice(obj.Object, "spec", "clusterConfig", "addons")
if err != nil || !found {
return fmt.Errorf("addons is not found in configmap %s", patchConfigMapName)
}
updateCRDAddonsWithPatch(addonsRaw, patchAddons)
err = unstructured.SetNestedSlice(obj.Object, addonsRaw, "spec", "clusterConfig", "addons")
if err != nil {
return errors.Wrap(err, "failed to update cluster configuration with patch version info")
}
return nil
}
func getAddonsFromPatchCM(clientSet kubernetes.Interface, patchConfigMapName string) ([]AddonInfo, error) {
ns := constant.PatchNameSpace
cm, err := clientSet.CoreV1().ConfigMaps(ns).Get(context.TODO(), patchConfigMapName, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrapf(err, "failed to get config map '%s'", patchConfigMapName)
}
version := strings.TrimPrefix(patchConfigMapName, constant.PatchValuePrefix)
patchYAML, exists := cm.Data[version]
if !exists {
return nil, fmt.Errorf("key '%s' not found in configmap data", version)
}
var config PatchConfig
if err = yaml.Unmarshal([]byte(patchYAML), &config); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal patch config YAML")
}
return config.Addons, nil
}
func patchCoreDNSAntiAffinity(obj *unstructured.Unstructured) error {
nodes, found, err := unstructured.NestedSlice(obj.Object, "spec", "clusterConfig", "nodes")
if err != nil {
return errors.Wrap(err, "failed to get nodes from cluster config")
}
if !found || len(nodes) == 0 {
zlog.Warn("no nodes found in cluster config, skipping CoreDNS anti-affinity patch")
return nil
}
nodeCount := len(nodes)
enableAntiAffinity := "false"
if nodeCount > 1 {
enableAntiAffinity = "true"
}
zlog.Infof("Cluster has %d nodes, setting CoreDNS EnableAntiAffinity to %s", nodeCount, enableAntiAffinity)
addons, found, err := unstructured.NestedSlice(obj.Object, "spec", "clusterConfig", "addons")
if err != nil {
return errors.Wrap(err, "failed to get addons from cluster config")
}
if !found {
zlog.Warn("no addons found in cluster config, skipping CoreDNS anti-affinity patch")
return nil
}
updated := false
for i, addon := range addons {
addonMap, ok := addon.(map[string]interface{})
if !ok {
continue
}
name, ok := getString(addonMap, "name")
if !ok || name != "coredns" {
continue
}
param := getOrCreateParamMap(addonMap)
param["EnableAntiAffinity"] = enableAntiAffinity
addonMap["param"] = param
addons[i] = addonMap
updated = true
zlog.Infof("Updated CoreDNS addon EnableAntiAffinity to %s", enableAntiAffinity)
break
}
if !updated {
zlog.Warn("coredns addon not found in addons list")
return nil
}
if err := unstructured.SetNestedSlice(obj.Object, addons, "spec", "clusterConfig", "addons"); err != nil {
return errors.Wrap(err, "failed to update addons in cluster config")
}
return nil
}
func IsOnlineMode(cm *corev1.ConfigMap) (bool, error) {
if len(cm.Data) == 0 {
return false, errors.New("configMap.Data is nil or empty")
}
repoValue, repoOk := cm.Data["otherRepo"]
imageValue, imageOk := cm.Data["onlineImage"]
if !repoOk || !imageOk {
return false, fmt.Errorf("configMap key `otherRepo` or `onlineImage` missing")
} else if repoValue == "" && imageValue == "" {
return false, nil
}
return true, nil
}