istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/helm/helm.go (about)

     1  // Copyright Istio Authors
     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  package helm
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"helm.sh/helm/v3/pkg/chart"
    26  	"helm.sh/helm/v3/pkg/chartutil"
    27  	"helm.sh/helm/v3/pkg/engine"
    28  	"k8s.io/apimachinery/pkg/version"
    29  	"sigs.k8s.io/yaml"
    30  
    31  	"istio.io/istio/istioctl/pkg/install/k8sversion"
    32  	"istio.io/istio/manifests"
    33  	"istio.io/istio/operator/pkg/util"
    34  	"istio.io/istio/pkg/log"
    35  )
    36  
    37  const (
    38  	// YAMLSeparator is a separator for multi-document YAML files.
    39  	YAMLSeparator = "\n---\n"
    40  
    41  	// DefaultProfileString is the name of the default profile.
    42  	DefaultProfileString = "default"
    43  
    44  	// NotesFileNameSuffix is the file name suffix for helm notes.
    45  	// see https://helm.sh/docs/chart_template_guide/notes_files/
    46  	NotesFileNameSuffix = ".txt"
    47  )
    48  
    49  var scope = log.RegisterScope("installer", "installer")
    50  
    51  // TemplateFilterFunc filters templates to render by their file name
    52  type TemplateFilterFunc func(string) bool
    53  
    54  // TemplateRenderer defines a helm template renderer interface.
    55  type TemplateRenderer interface {
    56  	// Run starts the renderer and should be called before using it.
    57  	Run() error
    58  	// RenderManifest renders the associated helm charts with the given values YAML string and returns the resulting
    59  	// string.
    60  	RenderManifest(values string) (string, error)
    61  	// RenderManifestFiltered filters manifests to render by template file name
    62  	RenderManifestFiltered(values string, filter TemplateFilterFunc) (string, error)
    63  }
    64  
    65  // NewHelmRenderer creates a new helm renderer with the given parameters and returns an interface to it.
    66  // The format of helmBaseDir and profile strings determines the type of helm renderer returned (compiled-in, file,
    67  // HTTP etc.)
    68  func NewHelmRenderer(operatorDataDir, helmSubdir, componentName, namespace string, version *version.Info) TemplateRenderer {
    69  	dir := strings.Join([]string{ChartsSubdirName, helmSubdir}, "/")
    70  	return NewGenericRenderer(manifests.BuiltinOrDir(operatorDataDir), dir, componentName, namespace, version)
    71  }
    72  
    73  // ReadProfileYAML reads the YAML values associated with the given profile. It uses an appropriate reader for the
    74  // profile format (compiled-in, file, HTTP, etc.).
    75  func ReadProfileYAML(profile, manifestsPath string) (string, error) {
    76  	var err error
    77  	var globalValues string
    78  
    79  	// Get global values from profile.
    80  	switch {
    81  	case util.IsFilePath(profile):
    82  		if globalValues, err = readFile(profile); err != nil {
    83  			return "", err
    84  		}
    85  	default:
    86  		if globalValues, err = LoadValues(profile, manifestsPath); err != nil {
    87  			return "", fmt.Errorf("failed to read profile %v from %v: %v", profile, manifestsPath, err)
    88  		}
    89  	}
    90  
    91  	return globalValues, nil
    92  }
    93  
    94  // renderChart renders the given chart with the given values and returns the resulting YAML manifest string.
    95  func renderChart(namespace, values string, chrt *chart.Chart, filterFunc TemplateFilterFunc, version *version.Info) (string, error) {
    96  	options := chartutil.ReleaseOptions{
    97  		Name:      "istio",
    98  		Namespace: namespace,
    99  	}
   100  	valuesMap := map[string]any{}
   101  	if err := yaml.Unmarshal([]byte(values), &valuesMap); err != nil {
   102  		return "", fmt.Errorf("failed to unmarshal values: %v", err)
   103  	}
   104  
   105  	caps := *chartutil.DefaultCapabilities
   106  
   107  	// overwrite helm default capabilities
   108  	operatorVersion, _ := chartutil.ParseKubeVersion("1." + strconv.Itoa(k8sversion.MinK8SVersion) + ".0")
   109  	caps.KubeVersion = *operatorVersion
   110  
   111  	if version != nil {
   112  		caps.KubeVersion = chartutil.KubeVersion{
   113  			Version: version.GitVersion,
   114  			Major:   version.Major,
   115  			Minor:   version.Minor,
   116  		}
   117  	}
   118  	vals, err := chartutil.ToRenderValues(chrt, valuesMap, options, &caps)
   119  	if err != nil {
   120  		return "", err
   121  	}
   122  
   123  	if filterFunc != nil {
   124  		filteredTemplates := []*chart.File{}
   125  		for _, t := range chrt.Templates {
   126  			// Always include required templates that do not produce any output
   127  			if filterFunc(t.Name) || strings.HasSuffix(t.Name, ".tpl") || t.Name == "templates/zzz_profile.yaml" {
   128  				filteredTemplates = append(filteredTemplates, t)
   129  			}
   130  		}
   131  		chrt.Templates = filteredTemplates
   132  	}
   133  
   134  	files, err := engine.Render(chrt, vals)
   135  	crdFiles := chrt.CRDObjects()
   136  	if err != nil {
   137  		return "", err
   138  	}
   139  	if chrt.Metadata.Name == "base" {
   140  		base, _ := valuesMap["base"].(map[string]any)
   141  		if enableIstioConfigCRDs, ok := base["enableIstioConfigCRDs"].(bool); ok && !enableIstioConfigCRDs {
   142  			crdFiles = []chart.CRD{}
   143  		}
   144  	}
   145  
   146  	// Create sorted array of keys to iterate over, to stabilize the order of the rendered templates
   147  	keys := make([]string, 0, len(files))
   148  	for k := range files {
   149  		if strings.HasSuffix(k, NotesFileNameSuffix) {
   150  			continue
   151  		}
   152  		keys = append(keys, k)
   153  	}
   154  	sort.Strings(keys)
   155  
   156  	var sb strings.Builder
   157  	for i := 0; i < len(keys); i++ {
   158  		f := files[keys[i]]
   159  		// add yaml separator if the rendered file doesn't have one at the end
   160  		f = strings.TrimSpace(f) + "\n"
   161  		if !strings.HasSuffix(f, YAMLSeparator) {
   162  			f += YAMLSeparator
   163  		}
   164  		_, err := sb.WriteString(f)
   165  		if err != nil {
   166  			return "", err
   167  		}
   168  	}
   169  
   170  	// Sort crd files by name to ensure stable manifest output
   171  	sort.Slice(crdFiles, func(i, j int) bool { return crdFiles[i].Name < crdFiles[j].Name })
   172  	for _, crdFile := range crdFiles {
   173  		f := string(crdFile.File.Data)
   174  		// add yaml separator if the rendered file doesn't have one at the end
   175  		f = strings.TrimSpace(f) + "\n"
   176  		if !strings.HasSuffix(f, YAMLSeparator) {
   177  			f += YAMLSeparator
   178  		}
   179  		_, err := sb.WriteString(f)
   180  		if err != nil {
   181  			return "", err
   182  		}
   183  	}
   184  
   185  	return sb.String(), nil
   186  }
   187  
   188  // GenerateHubTagOverlay creates an IstioOperatorSpec overlay YAML for hub and tag.
   189  func GenerateHubTagOverlay(hub, tag string) (string, error) {
   190  	hubTagYAMLTemplate := `
   191  spec:
   192    hub: {{.Hub}}
   193    tag: {{.Tag}}
   194  `
   195  	ts := struct {
   196  		Hub string
   197  		Tag string
   198  	}{
   199  		Hub: hub,
   200  		Tag: tag,
   201  	}
   202  	return util.RenderTemplate(hubTagYAMLTemplate, ts)
   203  }
   204  
   205  // DefaultFilenameForProfile returns the profile name of the default profile for the given profile.
   206  func DefaultFilenameForProfile(profile string) string {
   207  	switch {
   208  	case util.IsFilePath(profile):
   209  		return filepath.Join(filepath.Dir(profile), DefaultProfileFilename)
   210  	default:
   211  		return DefaultProfileString
   212  	}
   213  }
   214  
   215  // IsDefaultProfile reports whether the given profile is the default profile.
   216  func IsDefaultProfile(profile string) bool {
   217  	return profile == "" || profile == DefaultProfileString || filepath.Base(profile) == DefaultProfileFilename
   218  }
   219  
   220  func readFile(path string) (string, error) {
   221  	b, err := os.ReadFile(path)
   222  	return string(b), err
   223  }
   224  
   225  // GetProfileYAML returns the YAML for the given profile name, using the given profileOrPath string, which may be either
   226  // a profile label or a file path.
   227  func GetProfileYAML(installPackagePath, profileOrPath string) (string, error) {
   228  	if profileOrPath == "" {
   229  		profileOrPath = "default"
   230  	}
   231  	profiles, err := readProfiles(installPackagePath)
   232  	if err != nil {
   233  		return "", fmt.Errorf("failed to read profiles: %v", err)
   234  	}
   235  	// If charts are a file path and profile is a name like default, transform it to the file path.
   236  	if profiles[profileOrPath] && installPackagePath != "" {
   237  		profileOrPath = filepath.Join(installPackagePath, "profiles", profileOrPath+".yaml")
   238  	}
   239  	// This contains the IstioOperator CR.
   240  	baseCRYAML, err := ReadProfileYAML(profileOrPath, installPackagePath)
   241  	if err != nil {
   242  		return "", err
   243  	}
   244  
   245  	if !IsDefaultProfile(profileOrPath) {
   246  		// Profile definitions are relative to the default profileOrPath, so read that first.
   247  		dfn := DefaultFilenameForProfile(profileOrPath)
   248  		defaultYAML, err := ReadProfileYAML(dfn, installPackagePath)
   249  		if err != nil {
   250  			return "", err
   251  		}
   252  		baseCRYAML, err = util.OverlayIOP(defaultYAML, baseCRYAML)
   253  		if err != nil {
   254  			return "", err
   255  		}
   256  	}
   257  
   258  	return baseCRYAML, nil
   259  }