...

Source file src/edge-infra.dev/pkg/edge/linkerd/helm/render/render.go

Documentation: edge-infra.dev/pkg/edge/linkerd/helm/render

     1  // Package render contains functions for rendering the Linkerd Helm Chart.
     2  package render
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"net/http"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/linkerd/linkerd2/pkg/charts"
    13  	"helm.sh/helm/v3/pkg/chart/loader"
    14  	"helm.sh/helm/v3/pkg/chartutil"
    15  	"helm.sh/helm/v3/pkg/engine"
    16  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    17  
    18  	linkerd "edge-infra.dev/pkg/edge/linkerd"
    19  	l5dvalues "edge-infra.dev/pkg/edge/linkerd/helm/values"
    20  	"edge-infra.dev/pkg/edge/linkerd/policy"
    21  	"edge-infra.dev/pkg/k8s/decoder"
    22  	l5dchart "edge-infra.dev/third_party/k8s/linkerd/helm"
    23  )
    24  
    25  // Option is a function that mutates the Helm Chart values that will be
    26  // passed to engine.Render()
    27  type Option func(*l5dvalues.Values)
    28  
    29  // Sets linkerd control plane priority class
    30  func WithPriorityClass(priorityClass string) Option {
    31  	return func(v *l5dvalues.Values) {
    32  		v.PriorityClassName = priorityClass
    33  	}
    34  }
    35  
    36  // Sets proxy iptables mode
    37  func WithProxyIptablesMode(mode string) Option {
    38  	return func(v *l5dvalues.Values) {
    39  		v.ProxyInit.IptablesMode = mode
    40  	}
    41  }
    42  
    43  // WithExternalIDIssuer configures Linkerd to use an external CA crt and use
    44  // K8s TLS secrets for the issuer scheme
    45  // https://linkerd.io/2.11/tasks/automatically-rotating-control-plane-tls-credentials/#installing-with-helm
    46  func WithExternalIDIssuer(crt string) Option {
    47  	return func(v *l5dvalues.Values) {
    48  		v.IdentityTrustAnchorsPEM = crt
    49  		v.Identity.Issuer.Scheme = "kubernetes.io/tls"
    50  	}
    51  }
    52  
    53  // WithHighAvailability configures Linkerd to run in High Availability mode
    54  func WithHighAvailability() Option {
    55  	return func(v *l5dvalues.Values) {
    56  		v.HighAvailability = true
    57  	}
    58  }
    59  
    60  // WithAuthPolicy allows setting the default policy for a Linkerd installation:
    61  // https://linkerd.io/2.11/reference/authorization-policy/
    62  func WithAuthPolicy(policy string) Option {
    63  	return func(v *l5dvalues.Values) {
    64  		v.Proxy.DefaultInboundPolicy = policy
    65  	}
    66  }
    67  
    68  // WithClusterNetworks allows to set the cluster networks
    69  func WithClusterNetworks(clusterNetworks string) Option {
    70  	return func(v *l5dvalues.Values) {
    71  		v.ClusterNetworks = clusterNetworks
    72  	}
    73  }
    74  
    75  // WithContainerRegistry overrides the source registry for the Linkerd images,
    76  // and the pull secret used to pull them.  If a pull secret is not provided,
    77  // the `imagePullSecrets` field in Linkerd's values.yaml is not updated.
    78  func WithContainerRegistry(registry string, pullSecretName string) Option {
    79  	return func(v *l5dvalues.Values) {
    80  		v.PolicyController.Image.Name = path.Join(registry, l5dchart.PolicyControllerImage)
    81  		v.ProxyInit.Image.Name = path.Join(registry, l5dchart.ProxyInitImage)
    82  		v.DebugContainer.Image.Name = path.Join(registry, l5dchart.DebugImage)
    83  		v.ControllerImage = path.Join(registry, l5dchart.ControllerImage)
    84  
    85  		// See cmd/edge/linkerdctl/linkerd-proxy/BUILD.bazel for more on building the proxy image
    86  		// we currently have to override an env var to support lan outage
    87  		v.Proxy.Image.Name = path.Join(registry, "proxy-ha")
    88  
    89  		if pullSecretName != "" {
    90  			v.ImagePullSecrets = []map[string]string{
    91  				{"name": pullSecretName},
    92  			}
    93  		}
    94  	}
    95  }
    96  
    97  // WithLogLevel allows setting a single log level for all components, with the
    98  // option of providing one or more Linkerd logging expressions for the Rust control
    99  // plane components which support the logging expressions described at
   100  // https://linkerd.io/2.11/reference/proxy-log-level/. Expressions are apended
   101  // to the provided log level to create the full passed in expression.
   102  //
   103  // Because the Rust logger used by L5d allows controlling logs by library, this
   104  // may not be suitable for your use case if you need to control logging for a module
   105  // that only exists in one Rust L5d component. See WithProxyLogLevel and
   106  // WithPolicyControllerLogLevel for alternatives.
   107  func WithLogLevel(level string, expr ...string) Option {
   108  	return func(v *l5dvalues.Values) {
   109  		// only sets log level for go components, despite name...
   110  		v.ControllerLogLevel = level
   111  
   112  		e := level
   113  		if len(expr) > 0 {
   114  			e = e + "," + strings.Join(expr, ",")
   115  		}
   116  		v.Proxy.LogLevel = e
   117  		v.PolicyController.LogLevel = e
   118  	}
   119  }
   120  
   121  // WithProxyLogLevel sets the logging expression for the L5d proxy sidecar
   122  // per https://linkerd.io/2.11/reference/proxy-log-level/
   123  func WithProxyLogLevel(expr string) Option {
   124  	return func(v *l5dvalues.Values) {
   125  		v.Proxy.LogLevel = expr
   126  	}
   127  }
   128  
   129  // WithPolicyControllerLogLevel sets the logging expression for the L5d policy
   130  // controller per https://linkerd.io/2.11/reference/proxy-log-level/
   131  func WithPolicyControllerLogLevel(expr string) Option {
   132  	return func(v *l5dvalues.Values) {
   133  		v.PolicyController.LogLevel = expr
   134  	}
   135  }
   136  
   137  // WithLogFormat sets the log formatting for all L5d components.
   138  func WithLogFormat(format string) Option {
   139  	return func(v *l5dvalues.Values) {
   140  		v.ControllerLogFormat = format
   141  		v.Proxy.LogFormat = format
   142  		v.ProxyInit.LogFormat = format
   143  	}
   144  }
   145  
   146  // WithProxyInitRunAsRoot ensures proxy init RunAsRoot for SDS compatibility
   147  func WithProxyInitRunAsRoot() Option {
   148  	return func(v *l5dvalues.Values) {
   149  		v.ProxyInit.RunAsRoot = true
   150  	}
   151  }
   152  
   153  // WithProxyInitResourceRequest sets resource request/limit for proxy init container
   154  func WithProxyInitResourceRequest() Option {
   155  	return func(v *l5dvalues.Values) {
   156  		// the request was increased to match limit in https://github.com/linkerd/linkerd2/pull/7989
   157  		// it doesn't seem to be necessary for edge use case
   158  		// lower it so that Jarvis (15 pods) can be deployed on resource very limited machines
   159  		v.ProxyInit.Resources.CPU.Request = "10m"
   160  	}
   161  }
   162  
   163  // In thick-pos (LAN-outage) mode we need all Linkerd control plane components to run on each node
   164  func WithThickPosConfiguration(issuanceLifetime string) Option {
   165  	return func(v *l5dvalues.Values) {
   166  		v.Identity.Issuer.IssuanceLifetime = issuanceLifetime // proxy certificates have increased duration
   167  	}
   168  }
   169  
   170  // Sets native side-car option for the proxy.
   171  // Enabled if all nodes >= K8s 1.29
   172  func WithNativeSidecar(enabled bool) Option {
   173  	return func(v *l5dvalues.Values) {
   174  		v.Proxy.NativeSidecar = enabled
   175  	}
   176  }
   177  
   178  // Manifests produces Linkerd2 installation manifests by rendering the Helm
   179  // Chart with the provided options
   180  func Manifests(opts ...Option) (map[string]string, error) {
   181  	values, err := l5dchart.DefaultValues()
   182  	if err != nil {
   183  		return nil, fmt.Errorf("failed to load charts default values: %w", err)
   184  	}
   185  
   186  	// append user provided options to default rendering options for all
   187  	// edge-managed installations of l5d. user options are appended after defaults
   188  	// to allow users to have last-say
   189  	opts = append([]Option{
   190  		// JSON logs at warn or greater enabled, add "error" for L5d components which
   191  		// accept log expressions, since their Rust logger seems to require explicit
   192  		// enablement per level
   193  		WithLogFormat(linkerd.JSONLogFormat),
   194  		WithProxyLogLevel("warn,error"),
   195  		// Lock down mesh by default
   196  		WithAuthPolicy(policy.Default),
   197  		WithProxyInitRunAsRoot(),
   198  		WithProxyInitResourceRequest(),
   199  		WithProxyIptablesMode(linkerd.IptablesModes),
   200  	}, opts...)
   201  
   202  	// apply our options to mutate our charts values before rendering the
   203  	// manifests
   204  	for _, o := range opts {
   205  		o(values)
   206  	}
   207  
   208  	files, err := readL5dFiles()
   209  	if err != nil {
   210  		return nil, fmt.Errorf("Failed reading in linkerd manifests: %w", err)
   211  	}
   212  
   213  	partials, err := readPartialFiles()
   214  	if err != nil {
   215  		return nil, fmt.Errorf("Failed reading in linkerd partials: %w", err)
   216  	}
   217  
   218  	chart, err := loader.LoadFiles(append(files, partials...))
   219  	if err != nil {
   220  		return nil, fmt.Errorf("Failed loading manifests: %w", err)
   221  	}
   222  
   223  	vmap, err := values.ToMap()
   224  	if err != nil {
   225  		return nil, fmt.Errorf("failed to convert values into map: %w", err)
   226  	}
   227  
   228  	rendered, err := engine.Render(chart, map[string]interface{}{"Values": vmap, "Release": map[string]interface{}{"Namespace": "linkerd"}})
   229  	if err != nil {
   230  		return nil, fmt.Errorf("failed to render manifests: %w", err)
   231  	}
   232  	return rendered, nil
   233  }
   234  
   235  // Unsructured renders the Linkerd manifests using Manifests() and then converts
   236  // them into K8s resource objects that can be applied to the API server
   237  func Unstructured(opts ...Option) ([]*unstructured.Unstructured, error) {
   238  	manifests, err := Manifests(opts...)
   239  	if err != nil {
   240  		return nil, fmt.Errorf("failed to render manifests: %w", err)
   241  	}
   242  
   243  	var buf bytes.Buffer
   244  	// can use file name to infer type and improve parsing code
   245  	for filepath, contents := range manifests {
   246  		// don't include non-resource files (e.g., NOTES.txt)
   247  		if !strings.HasSuffix(filepath, ".yaml") {
   248  			continue
   249  		}
   250  		if _, err := buf.WriteString(contents); err != nil {
   251  			return nil, fmt.Errorf("failed to write rendered manifest to buffer: %w", err)
   252  		}
   253  	}
   254  
   255  	resources, err := decoder.DecodeYAML(buf.Bytes())
   256  	if err != nil {
   257  		return nil, fmt.Errorf("failed to decode yaml data into struct: %w", err)
   258  	}
   259  
   260  	return resources, nil
   261  }
   262  
   263  func readL5dFiles() ([]*loader.BufferedFile, error) {
   264  	controlFiles := []*loader.BufferedFile{
   265  		{Name: chartutil.ChartfileName},
   266  		{Name: chartutil.ValuesfileName},
   267  	}
   268  
   269  	control := filepath.Join(l5dchart.ControlPath, chartutil.TemplatesDir)
   270  	controlTemplates, err := l5dchart.Templates.ReadDir(control)
   271  	if err != nil {
   272  		return nil, fmt.Errorf("failed to read %s: %w", l5dchart.ControlPath, err)
   273  	}
   274  
   275  	for _, p := range controlTemplates {
   276  		controlFiles = append(controlFiles, &loader.BufferedFile{
   277  			Name: filepath.Join(chartutil.TemplatesDir, p.Name()),
   278  		})
   279  	}
   280  
   281  	if err := charts.FilesReader(http.FS(l5dchart.Templates), l5dchart.ControlPath+"/", controlFiles); err != nil {
   282  		return nil, fmt.Errorf("failed to read linkerd chart into buffered files: %w", err)
   283  	}
   284  
   285  	return controlFiles, nil
   286  }
   287  
   288  func readPartialFiles() ([]*loader.BufferedFile, error) {
   289  	files := []*loader.BufferedFile{
   290  		{Name: chartutil.ChartfileName},
   291  		{Name: chartutil.ValuesfileName},
   292  	}
   293  
   294  	partials := filepath.Join(l5dchart.PartialsChartPath, chartutil.TemplatesDir)
   295  	L5dPartials, err := l5dchart.Templates.ReadDir(partials)
   296  	if err != nil {
   297  		return nil, fmt.Errorf("failed to read %s: %w", l5dchart.PartialsChartPath, err)
   298  	}
   299  
   300  	for _, t := range L5dPartials {
   301  		files = append(files, &loader.BufferedFile{
   302  			Name: filepath.Join(chartutil.TemplatesDir, t.Name()),
   303  		})
   304  	}
   305  
   306  	// provide chart path as directory argument so that the relative paths
   307  	// are read correctly
   308  	if err := charts.FilesReader(http.FS(l5dchart.Templates), l5dchart.PartialsChartPath+"/", files); err != nil {
   309  		return nil, fmt.Errorf("failed to read partials chart into buffered files: %w", err)
   310  	}
   311  
   312  	return files, nil
   313  }
   314  

View as plain text