Files
open-cluster-management/pkg/operator/helpers/chart/render.go
2025-04-09 07:46:27 +00:00

249 lines
6.9 KiB
Go

package chart
import (
"embed"
"fmt"
"io/fs"
"path/filepath"
"strings"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/engine"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/klog/v2"
"sigs.k8s.io/yaml"
clustermanagerchart "open-cluster-management.io/ocm/deploy/cluster-manager/chart"
klusterletchart "open-cluster-management.io/ocm/deploy/klusterlet/chart"
)
func NewDefaultClusterManagerChartConfig() *ClusterManagerChartConfig {
return &ClusterManagerChartConfig{
ReplicaCount: 3,
CreateBootstrapToken: false,
ClusterManager: ClusterManagerConfig{
Create: true,
},
}
}
func NewDefaultKlusterletChartConfig() *KlusterletChartConfig {
return &KlusterletChartConfig{
ReplicaCount: 1,
Klusterlet: KlusterletConfig{
Create: true,
},
}
}
// RenderClusterManagerChart renders the ClusterManager objects to be created on the hub.
// It returns three values: CRD objects(which usually should be created before other objects), other Kubernetes objects, error.
func RenderClusterManagerChart(config *ClusterManagerChartConfig, namespace string) ([][]byte, [][]byte, error) {
if namespace == "" {
return nil, nil, fmt.Errorf("cluster manager chart namespace is required")
}
return renderChart(config, namespace, config.CreateNamespace,
clustermanagerchart.ChartName, clustermanagerchart.ChartFiles)
}
// RenderKlusterletChart renders the Klusterlet objects to be created on the managed cluster.
// It returns three values: CRD objects(which usually should be created before other objects), other Kubernetes objects, error.
func RenderKlusterletChart(config *KlusterletChartConfig, namespace string) ([][]byte, [][]byte, error) {
if namespace == "" {
return nil, nil, fmt.Errorf("klusterlet chart namespace is required")
}
return renderChart(config, namespace, config.CreateNamespace,
klusterletchart.ChartName, klusterletchart.ChartFiles)
}
func renderChart[T *ClusterManagerChartConfig | *KlusterletChartConfig](config T,
namespace string, createNamespace bool, chartName string, fs embed.FS) ([][]byte, [][]byte, error) {
// chartName is the prefix of chart path here
operatorChart, err := LoadChart(fs, chartName)
if err != nil {
return nil, nil, fmt.Errorf("failed to load %s chart: %w", chartName, err)
}
configValues, err := JsonStructToValues(config)
if err != nil {
return nil, nil, fmt.Errorf("error generating values for chartConfig: %v", err)
}
releaseOptions := chartutil.ReleaseOptions{
Name: chartName,
Namespace: namespace,
}
values, err := chartutil.ToRenderValues(operatorChart, configValues,
releaseOptions, &chartutil.Capabilities{})
if err != nil {
klog.Errorf("failed to render helm chart with values %v. err:%v", values, err)
return nil, nil, err
}
crdObjects, rawObjects, err := renderManifests(operatorChart, values)
if err != nil {
return nil, nil, fmt.Errorf("error rendering cluster manager chart: %v", err)
}
// make sure the ns object is at the top of slice when createNamespace is true.
rstObjects := [][]byte{}
if createNamespace {
nsObj, err := newNamespaceRawObject(namespace)
if err != nil {
return nil, nil, err
}
rstObjects = [][]byte{nsObj}
}
rstObjects = append(rstObjects, rawObjects...)
return crdObjects, rstObjects, nil
}
func getFiles(manifestFS embed.FS) ([]string, error) {
var res []string
err := fs.WalkDir(manifestFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
res = append(res, path)
return nil
})
return res, err
}
func stripPrefix(chartPrefix, path string) string {
prefixNoPathSeparatorSuffix := strings.TrimSuffix(chartPrefix, string(filepath.Separator))
chartPrefixLen := len(strings.Split(prefixNoPathSeparatorSuffix, string(filepath.Separator)))
pathValues := strings.Split(path, string(filepath.Separator))
return strings.Join(pathValues[chartPrefixLen:], string(filepath.Separator))
}
func LoadChart(chartFS embed.FS, chartPrefix string) (*chart.Chart, error) {
files, err := getFiles(chartFS)
if err != nil {
return nil, err
}
var bfs []*loader.BufferedFile
for _, fileName := range files {
b, err := fs.ReadFile(chartFS, fileName)
if err != nil {
klog.Errorf("failed to read file %v. err:%v", fileName, err)
return nil, err
}
if !strings.HasPrefix(fileName, chartPrefix) {
continue
}
bf := &loader.BufferedFile{
Name: stripPrefix(chartPrefix, fileName),
Data: b,
}
bfs = append(bfs, bf)
}
userChart, err := loader.LoadFiles(bfs)
if err != nil {
klog.Errorf("failed to load chart. err:%v", err)
return nil, err
}
return userChart, nil
}
// JsonStructToValues converts the given json struct to a Values
func JsonStructToValues(a interface{}) (chartutil.Values, error) {
raw, err := json.Marshal(a)
if err != nil {
return nil, err
}
vals, err := chartutil.ReadValues(raw)
if err != nil {
return nil, err
}
return vals, nil
}
func renderManifests(chart *chart.Chart, values chartutil.Values) ([][]byte, [][]byte, error) {
var rawCRDObjects, rawObjects [][]byte
// make sure the CRDs are at the top.
crds := chart.CRDObjects()
for _, crd := range crds {
klog.V(4).Infof("%v/n", crd.File.Data)
rawCRDObjects = append(rawCRDObjects, crd.File.Data)
}
helmEngine := engine.Engine{
Strict: true,
LintMode: false,
}
templates, err := helmEngine.Render(chart, values)
if err != nil {
return rawCRDObjects, rawObjects, err
}
namespaceObjects := [][]byte{}
for _, template := range templates {
// skip the template only including `\n`
if len(template) < 2 {
continue
}
objects := strings.Split(template, "---")
for _, data := range objects {
// skip the data only including `\n`
if len(data) < 2 {
continue
}
// remove invalid template
unstructuredObj := &unstructured.Unstructured{}
if err = yaml.Unmarshal([]byte(data), unstructuredObj); err != nil {
return nil, nil, fmt.Errorf("error unmarshalling template: %v", err)
}
kind := unstructuredObj.GetKind()
if kind == "" {
continue
}
if kind == "Namespace" {
namespaceObjects = append(namespaceObjects, []byte(data))
continue
}
rawObjects = append(rawObjects, []byte(data))
}
}
// will create open-cluster-management-agent ns in klusterlet operator,
// so need make sure namespaces are at the top of slice.
if len(namespaceObjects) != 0 {
result := append(namespaceObjects, rawObjects...) //nolint:gocritic
return rawCRDObjects, result, nil
}
return rawCRDObjects, rawObjects, nil
}
func newNamespaceRawObject(namespace string) ([]byte, error) {
ns := &corev1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}
return yaml.Marshal(ns)
}