added --set flag to install-cni plugin (#10633)

This PR added support for --set flag to linkerd cni-plugin installation command.
Also made changes to test file for cni-plugin install.
Fixed a bug at pkg/chart/charts.go for resources template.
fixes #9917

* Allow supporting all flags and values

This leverages `chartutil.CoalesceValues` in order to merge the values provided through regular flags, and the ones provided via `--set` flags. As that function consumes maps, I introduced the `ToMap` method function on the cni `Values` struct (a copy of the same function from the core linkerd `Values` struct) to convert the struct backing the regular flags into a map.

And for the `RenderCNI` method to be able to deal with value maps instead of yaml, the `charts.Chart` struct now distinguishes between `Values` (a map) and `RawValues` (YAML).

This allowed removing the `parseYAMLValue` function and avoid having to deal with individual entries in `buildValues()`, and we no longer need the `valuesOverrides` field in the `cniPluginOptions` struct.

## Tests

```bash
# Testing regular flag
$ bin/go-run cli install-cni --use-wait-flag | grep use.wait.flag
        "use-wait-flag": true

# Testing using --set
$ bin/go-run cli install-cni --set useWaitFlag=true | grep use.wait.flag
        "use-wait-flag": true

# Testing using --set on a setting that has no regular flag
$ bin/go-run cli install-cni --set enablePSP=true | grep PodSecurityPolicy
kind: PodSecurityPolicy
```

---------

Signed-off-by: amit-62 <kramit6662@gmail.com>
Co-authored-by: Alejandro Pedraza <alejandro.pedraza@gmail.com>
Co-authored-by: Matei David <matei.david.35@gmail.com>
This commit is contained in:
Amit Kumar 2023-04-20 20:04:06 +05:30 committed by GitHub
parent 8510ea2e19
commit d26c324e76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 134 additions and 47 deletions

View File

@ -32,7 +32,7 @@ Kubernetes: `>=1.21.0-0`
| image.name | string | `"cr.l5d.io/linkerd/cni-plugin"` | Docker image for the CNI plugin | | image.name | string | `"cr.l5d.io/linkerd/cni-plugin"` | Docker image for the CNI plugin |
| image.pullPolicy | string | `"IfNotPresent"` | Pull policy for the linkerd-cni container | | image.pullPolicy | string | `"IfNotPresent"` | Pull policy for the linkerd-cni container |
| image.version | string | `"v1.1.0"` | Tag for the CNI container Docker image | | image.version | string | `"v1.1.0"` | Tag for the CNI container Docker image |
| imagePullSecrets | string | `nil` | | | imagePullSecrets | list | `[]` | |
| inboundProxyPort | int | `4143` | Inbound port for the proxy container | | inboundProxyPort | int | `4143` | Inbound port for the proxy container |
| logLevel | string | `"info"` | Log level for the CNI plugin | | logLevel | string | `"info"` | Log level for the CNI plugin |
| outboundProxyPort | int | `4140` | Outbound port for the proxy container | | outboundProxyPort | int | `4140` | Outbound port for the proxy container |

View File

@ -67,7 +67,7 @@ image:
# #
# The pull secrets are applied to the respective service accounts # The pull secrets are applied to the respective service accounts
# which will further orchestrate the deployments. # which will further orchestrate the deployments.
imagePullSecrets: imagePullSecrets: []
# -- Add additional initContainers to the daemonset # -- Add additional initContainers to the daemonset
extraInitContainers: [] extraInitContainers: []

View File

@ -14,9 +14,10 @@ import (
"github.com/linkerd/linkerd2/pkg/version" "github.com/linkerd/linkerd2/pkg/version"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"sigs.k8s.io/yaml" "helm.sh/helm/v3/pkg/cli/values"
) )
const ( const (
@ -96,6 +97,7 @@ func newCmdInstallCNIPlugin() *cobra.Command {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
} }
var valOpts values.Options
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "install-cni [flags]", Use: "install-cni [flags]",
@ -106,9 +108,11 @@ This command installs a DaemonSet into the Linkerd control plane. The DaemonSet
copies the necessary linkerd-cni plugin binaries and configs onto the host. It copies the necessary linkerd-cni plugin binaries and configs onto the host. It
assumes that the 'linkerd install' command will be executed with the assumes that the 'linkerd install' command will be executed with the
'--linkerd-cni-enabled' flag. This command needs to be executed before the '--linkerd-cni-enabled' flag. This command needs to be executed before the
'linkerd install --linkerd-cni-enabled' command.`, 'linkerd install --linkerd-cni-enabled' command.
The installation can be configured by using the --set, --values, --set-string and --set-file flags. A full list of configurable values can be found at https://artifacthub.io/packages/helm/linkerd2/linkerd2-cni#values`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return renderCNIPlugin(os.Stdout, options) return renderCNIPlugin(os.Stdout, valOpts, options)
}, },
} }
@ -135,6 +139,8 @@ assumes that the 'linkerd install' command will be executed with the
options.useWaitFlag, options.useWaitFlag,
"Configures the CNI plugin to use the \"-w\" flag for the iptables command. (default false)") "Configures the CNI plugin to use the \"-w\" flag for the iptables command. (default false)")
flags.AddValueOptionsFlags(cmd.Flags(), &valOpts)
return cmd return cmd
} }
@ -204,19 +210,32 @@ func (options *cniPluginOptions) buildValues() (*cnicharts.Values, error) {
return installValues, nil return installValues, nil
} }
func renderCNIPlugin(w io.Writer, config *cniPluginOptions) error { func renderCNIPlugin(w io.Writer, valOpts values.Options, config *cniPluginOptions) error {
if err := config.validate(); err != nil { if err := config.validate(); err != nil {
return err return err
} }
valuesOverrides, err := valOpts.MergeValues(nil)
if err != nil {
return err
}
values, err := config.buildValues() values, err := config.buildValues()
if err != nil { if err != nil {
return err return err
} }
// Render raw values and create chart config mapValues, err := values.ToMap()
rawValues, err := yaml.Marshal(values) if err != nil {
return err
}
valuesWrapper := &chart.Chart{
Metadata: &chart.Metadata{Name: ""},
Values: mapValues,
}
mergedValues, err := chartutil.CoalesceValues(valuesWrapper, valuesOverrides)
if err != nil { if err != nil {
return err return err
} }
@ -226,15 +245,16 @@ func renderCNIPlugin(w io.Writer, config *cniPluginOptions) error {
{Name: "templates/cni-plugin.yaml"}, {Name: "templates/cni-plugin.yaml"},
} }
chart := &charts.Chart{ ch := &charts.Chart{
Name: helmCNIDefaultChartName, Name: helmCNIDefaultChartName,
Dir: helmCNIDefaultChartDir, Dir: helmCNIDefaultChartDir,
Namespace: defaultCNINamespace, Namespace: defaultCNINamespace,
RawValues: rawValues, Values: mergedValues,
Files: files, Files: files,
Fs: static.Templates, Fs: static.Templates,
} }
buf, err := chart.RenderCNI()
buf, err := ch.RenderCNI()
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"testing" "testing"
"helm.sh/helm/v3/pkg/cli/values"
) )
func TestRenderCNIPlugin(t *testing.T) { func TestRenderCNIPlugin(t *testing.T) {
@ -76,22 +78,27 @@ func TestRenderCNIPlugin(t *testing.T) {
defaultOptionsWithSkipPorts.ignoreInboundPorts = append(defaultOptionsWithSkipPorts.ignoreInboundPorts, []string{"80", "8080"}...) defaultOptionsWithSkipPorts.ignoreInboundPorts = append(defaultOptionsWithSkipPorts.ignoreInboundPorts, []string{"80", "8080"}...)
defaultOptionsWithSkipPorts.ignoreOutboundPorts = append(defaultOptionsWithSkipPorts.ignoreOutboundPorts, []string{"443", "1000"}...) defaultOptionsWithSkipPorts.ignoreOutboundPorts = append(defaultOptionsWithSkipPorts.ignoreOutboundPorts, []string{"443", "1000"}...)
valOpts := values.Options{
Values: []string{"resources.cpu.limit=1m"},
}
testCases := []struct { testCases := []struct {
*cniPluginOptions *cniPluginOptions
valOpts values.Options
goldenFileName string goldenFileName string
}{ }{
{defaultOptions, "install-cni-plugin_default.golden"}, {defaultOptions, valOpts, "install-cni-plugin_default.golden"},
{fullyConfiguredOptions, "install-cni-plugin_fully_configured.golden"}, {fullyConfiguredOptions, values.Options{}, "install-cni-plugin_fully_configured.golden"},
{fullyConfiguredOptionsEqualDsts, "install-cni-plugin_fully_configured_equal_dsts.golden"}, {fullyConfiguredOptionsEqualDsts, values.Options{}, "install-cni-plugin_fully_configured_equal_dsts.golden"},
{fullyConfiguredOptionsNoNamespace, "install-cni-plugin_fully_configured_no_namespace.golden"}, {fullyConfiguredOptionsNoNamespace, values.Options{}, "install-cni-plugin_fully_configured_no_namespace.golden"},
{defaultOptionsWithSkipPorts, "install-cni-plugin_skip_ports.golden"}, {defaultOptionsWithSkipPorts, values.Options{}, "install-cni-plugin_skip_ports.golden"},
} }
for i, tc := range testCases { for i, tc := range testCases {
tc := tc // pin tc := tc // pin
t.Run(fmt.Sprintf("%d: %s", i, tc.goldenFileName), func(t *testing.T) { t.Run(fmt.Sprintf("%d: %s", i, tc.goldenFileName), func(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
err := renderCNIPlugin(&buf, tc.cniPluginOptions) err := renderCNIPlugin(&buf, tc.valOpts, tc.cniPluginOptions)
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }

View File

@ -110,6 +110,7 @@ func chartCniPlugin(t *testing.T) *chart.Chart {
"templates/_helpers.tpl", "templates/_helpers.tpl",
"templates/_metadata.tpl", "templates/_metadata.tpl",
"templates/_tolerations.tpl", "templates/_tolerations.tpl",
"templates/_resources.tpl",
}) })
cniChart := &chart.Chart{ cniChart := &chart.Chart{

View File

@ -157,7 +157,10 @@ spec:
name: linkerd-tmp-dir name: linkerd-tmp-dir
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
privileged: privileged: false
resources:
limits:
cpu: "1m"
volumes: volumes:
- name: cni-bin-dir - name: cni-bin-dir
hostPath: hostPath:

View File

@ -158,7 +158,8 @@ spec:
name: linkerd-tmp-dir name: linkerd-tmp-dir
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
privileged: privileged: false
resources:
volumes: volumes:
- name: cni-bin-dir - name: cni-bin-dir
hostPath: hostPath:

View File

@ -156,7 +156,8 @@ spec:
name: linkerd-tmp-dir name: linkerd-tmp-dir
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
privileged: privileged: false
resources:
volumes: volumes:
- name: cni-net-dir - name: cni-net-dir
hostPath: hostPath:

View File

@ -158,7 +158,8 @@ spec:
name: linkerd-tmp-dir name: linkerd-tmp-dir
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
privileged: privileged: false
resources:
volumes: volumes:
- name: cni-bin-dir - name: cni-bin-dir
hostPath: hostPath:

View File

@ -158,7 +158,8 @@ spec:
name: linkerd-tmp-dir name: linkerd-tmp-dir
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
privileged: privileged: false
resources:
volumes: volumes:
- name: cni-bin-dir - name: cni-bin-dir
hostPath: hostPath:

View File

@ -150,7 +150,8 @@ spec:
name: linkerd-tmp-dir name: linkerd-tmp-dir
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
privileged: privileged: false
resources:
volumes: volumes:
- name: cni-bin-dir - name: cni-bin-dir
hostPath: hostPath:

View File

@ -151,7 +151,8 @@ spec:
name: linkerd-tmp-dir name: linkerd-tmp-dir
securityContext: securityContext:
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
privileged: privileged: false
resources:
volumes: volumes:
- name: cni-bin-dir - name: cni-bin-dir
hostPath: hostPath:

View File

@ -2,6 +2,7 @@ package charts
import ( import (
"bytes" "bytes"
"errors"
"net/http" "net/http"
"path" "path"
"strings" "strings"
@ -45,9 +46,17 @@ type Chart struct {
Name string Name string
Dir string Dir string
Namespace string Namespace string
// RawValues are yaml-formatted values entries. Either this or Values
// should be set, but not both
RawValues []byte RawValues []byte
Files []*loader.BufferedFile
Fs http.FileSystem // Values are the config key-value entries. Either this or RawValues should
// be set, but not both
Values map[string]any
Files []*loader.BufferedFile
Fs http.FileSystem
} }
func (c *Chart) render(partialsFiles []*loader.BufferedFile) (bytes.Buffer, error) { func (c *Chart) render(partialsFiles []*loader.BufferedFile) (bytes.Buffer, error) {
@ -73,13 +82,17 @@ func (c *Chart) render(partialsFiles []*loader.BufferedFile) (bytes.Buffer, erro
Namespace: c.Namespace, Namespace: c.Namespace,
} }
var rawMapValues map[string]interface{} if len(c.RawValues) > 0 {
err = yaml.Unmarshal(c.RawValues, &rawMapValues) if c.Values != nil {
if err != nil { return bytes.Buffer{}, errors.New("either RawValues or Values should be set, but not both")
return bytes.Buffer{}, err }
err = yaml.Unmarshal(c.RawValues, &c.Values)
if err != nil {
return bytes.Buffer{}, err
}
} }
valuesToRender, err := chartutil.ToRenderValues(chart, rawMapValues, releaseOptions, nil) valuesToRender, err := chartutil.ToRenderValues(chart, c.Values, releaseOptions, nil)
if err != nil { if err != nil {
return bytes.Buffer{}, err return bytes.Buffer{}, err
} }
@ -124,6 +137,7 @@ func (c *Chart) RenderCNI() (bytes.Buffer, error) {
{Name: "charts/partials/templates/_metadata.tpl"}, {Name: "charts/partials/templates/_metadata.tpl"},
{Name: "charts/partials/templates/_pull-secrets.tpl"}, {Name: "charts/partials/templates/_pull-secrets.tpl"},
{Name: "charts/partials/templates/_tolerations.tpl"}, {Name: "charts/partials/templates/_tolerations.tpl"},
{Name: "charts/partials/templates/_resources.tpl"},
} }
return c.render(cniPartials) return c.render(cniPartials)
} }

View File

@ -22,24 +22,44 @@ type Image struct {
PullPolicy interface{} `json:"pullPolicy"` PullPolicy interface{} `json:"pullPolicy"`
} }
// Constraints wraps the Limit and Request settings for computational resources
type Constraints struct {
Limit string `json:"limit"`
Request string `json:"request"`
}
// Resources represents the computational resources setup for a given container
type Resources struct {
CPU Constraints `json:"cpu"`
Memory Constraints `json:"memory"`
EphemeralStorage Constraints `json:"ephemeral-storage"`
}
// Values contains the top-level elements in the cni Helm chart // Values contains the top-level elements in the cni Helm chart
type Values struct { type Values struct {
InboundProxyPort uint `json:"inboundProxyPort"` InboundProxyPort uint `json:"inboundProxyPort"`
OutboundProxyPort uint `json:"outboundProxyPort"` OutboundProxyPort uint `json:"outboundProxyPort"`
IgnoreInboundPorts string `json:"ignoreInboundPorts"` IgnoreInboundPorts string `json:"ignoreInboundPorts"`
IgnoreOutboundPorts string `json:"ignoreOutboundPorts"` IgnoreOutboundPorts string `json:"ignoreOutboundPorts"`
CliVersion string `json:"cliVersion"` CliVersion string `json:"cliVersion"`
Image Image `json:"image"` Image Image `json:"image"`
LogLevel string `json:"logLevel"` LogLevel string `json:"logLevel"`
PortsToRedirect string `json:"portsToRedirect"` PortsToRedirect string `json:"portsToRedirect"`
ProxyUID int64 `json:"proxyUID"` ProxyUID int64 `json:"proxyUID"`
DestCNINetDir string `json:"destCNINetDir"` DestCNINetDir string `json:"destCNINetDir"`
DestCNIBinDir string `json:"destCNIBinDir"` DestCNIBinDir string `json:"destCNIBinDir"`
UseWaitFlag bool `json:"useWaitFlag"` UseWaitFlag bool `json:"useWaitFlag"`
PriorityClassName string `json:"priorityClassName"` PriorityClassName string `json:"priorityClassName"`
ProxyAdminPort string `json:"proxyAdminPort"` ProxyAdminPort string `json:"proxyAdminPort"`
ProxyControlPort string `json:"proxyControlPort"` ProxyControlPort string `json:"proxyControlPort"`
Tolerations []interface{} `json:"tolerations"` Tolerations []interface{} `json:"tolerations"`
PodLabels map[string]string `json:"podLabels"`
CommonLabels map[string]string `json:"commonLabels"`
ImagePullSecrets []map[string]string `json:"imagePullSecrets"`
ExtraInitContainers []interface{} `json:"extraInitContainers"`
EnablePSP bool `json:"enablePSP"`
Privileged bool `json:"privileged"`
Resources Resources `json:"resources"`
} }
// NewValues returns a new instance of the Values type. // NewValues returns a new instance of the Values type.
@ -54,6 +74,22 @@ func NewValues() (*Values, error) {
return v, nil return v, nil
} }
// ToMap converts Values into a map[string]interface{}
func (v *Values) ToMap() (map[string]interface{}, error) {
var valuesMap map[string]interface{}
rawValues, err := yaml.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal the values struct: %w", err)
}
err = yaml.Unmarshal(rawValues, &valuesMap)
if err != nil {
return nil, fmt.Errorf("failed to Unmarshal Values into a map: %w", err)
}
return valuesMap, nil
}
// readDefaults reads all the default variables from the values.yaml file. // readDefaults reads all the default variables from the values.yaml file.
// chartDir is the root directory of the Helm chart where values.yaml is. // chartDir is the root directory of the Helm chart where values.yaml is.
func readDefaults(chartDir string) (*Values, error) { func readDefaults(chartDir string) (*Values, error) {