linkerd2/cli/cmd/install_test.go

532 lines
18 KiB
Go

package cmd
import (
"bytes"
"context"
"fmt"
"path/filepath"
"testing"
"github.com/linkerd/linkerd2/controller/gen/config"
charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
)
const (
installProxyVersion = "install-proxy-version"
installControlPlaneVersion = "install-control-plane-version"
installDebugVersion = "install-debug-version"
)
func TestRender(t *testing.T) {
ctx := context.Background()
defaultOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
defaultValues, _, err := defaultOptions.validateAndBuild(ctx, "", nil)
if err != nil {
t.Fatalf("Unexpected error validating options: %v", err)
}
addFakeTLSSecrets(defaultValues)
configValues, _, err := defaultOptions.validateAndBuild(ctx, configStage, nil)
if err != nil {
t.Fatalf("Unexpected error validating options: %v", err)
}
addFakeTLSSecrets(configValues)
controlPlaneValues, _, err := defaultOptions.validateAndBuild(ctx, controlPlaneStage, nil)
if err != nil {
t.Fatalf("Unexpected error validating options: %v", err)
}
// A configuration that shows that all config setting strings are honored
// by `render()`.
metaOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
identityContext := toIdentityContext(&identityWithAnchorsAndTrustDomain{
TrustAnchorsPEM: "test-trust-anchor",
Identity: &charts.Identity{
Issuer: &charts.Issuer{
ClockSkewAllowance: "20s",
IssuanceLifetime: "86400s",
},
},
})
metaConfig := metaOptions.configs(identityContext)
metaConfig.Global.LinkerdNamespace = "Namespace"
metaValues := &charts.Values{
ControllerImage: "ControllerImage",
WebImage: "WebImage",
ControllerUID: 2103,
EnableH2Upgrade: true,
WebhookFailurePolicy: "WebhookFailurePolicy",
OmitWebhookSideEffects: false,
RestrictDashboardPrivileges: false,
InstallNamespace: true,
Identity: defaultValues.Identity,
NodeSelector: defaultValues.NodeSelector,
Tolerations: defaultValues.Tolerations,
Global: &charts.Global{
Namespace: "Namespace",
ClusterDomain: "cluster.local",
ImagePullPolicy: "ImagePullPolicy",
CliVersion: "CliVersion",
ControllerComponentLabel: "ControllerComponentLabel",
ControllerLogLevel: "ControllerLogLevel",
ControllerImageVersion: "ControllerImageVersion",
ControllerNamespaceLabel: "ControllerNamespaceLabel",
WorkloadNamespaceLabel: "WorkloadNamespaceLabel",
CreatedByAnnotation: "CreatedByAnnotation",
ProxyInjectAnnotation: "ProxyInjectAnnotation",
ProxyInjectDisabled: "ProxyInjectDisabled",
LinkerdNamespaceLabel: "LinkerdNamespaceLabel",
ProxyContainerName: "ProxyContainerName",
CNIEnabled: false,
IdentityTrustDomain: defaultValues.Global.IdentityTrustDomain,
IdentityTrustAnchorsPEM: defaultValues.Global.IdentityTrustAnchorsPEM,
Proxy: &charts.Proxy{
DestinationGetNetworks: "DestinationGetNetworks",
Image: &charts.Image{
Name: "ProxyImageName",
PullPolicy: "ImagePullPolicy",
Version: "ProxyVersion",
},
LogLevel: "warn,linkerd=info",
LogFormat: "plain",
Ports: &charts.Ports{
Admin: 4191,
Control: 4190,
Inbound: 4143,
Outbound: 4140,
},
UID: 2102,
Trace: &charts.Trace{},
},
ProxyInit: &charts.ProxyInit{
Image: &charts.Image{
Name: "ProxyInitImageName",
PullPolicy: "ImagePullPolicy",
Version: "ProxyInitVersion",
},
Resources: &charts.Resources{
CPU: charts.Constraints{
Limit: "100m",
Request: "10m",
},
Memory: charts.Constraints{
Limit: "50Mi",
Request: "10Mi",
},
},
XTMountPath: &charts.VolumeMountPath{
MountPath: "/run",
Name: "linkerd-proxy-init-xtables-lock",
},
},
},
Configs: charts.ConfigJSONs{
Global: "GlobalConfig",
Proxy: "ProxyConfig",
Install: "InstallConfig",
},
ControllerReplicas: 1,
ProxyInjector: defaultValues.ProxyInjector,
ProfileValidator: defaultValues.ProfileValidator,
Tap: defaultValues.Tap,
Dashboard: &charts.Dashboard{
Replicas: 1,
},
Prometheus: charts.Prometheus{
"enabled": true,
"image": "PrometheusImage",
},
Tracing: map[string]interface{}{
"enabled": false,
},
Grafana: defaultValues.Grafana,
}
haOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
haOptions.recordedFlags = []*config.Install_Flag{{Name: "ha", Value: "true"}}
haOptions.highAvailability = true
haValues, _, _ := haOptions.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(haValues)
haWithOverridesOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
haWithOverridesOptions.recordedFlags = []*config.Install_Flag{
{Name: "ha", Value: "true"},
{Name: "controller-replicas", Value: "2"},
{Name: "proxy-cpu-request", Value: "400m"},
{Name: "proxy-memory-request", Value: "300Mi"},
}
haWithOverridesOptions.highAvailability = true
haWithOverridesOptions.controllerReplicas = 2
haWithOverridesOptions.proxyCPURequest = "400m"
haWithOverridesOptions.proxyMemoryRequest = "300Mi"
haWithOverridesValues, _, _ := haWithOverridesOptions.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(haWithOverridesValues)
cniEnabledOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
cniEnabledOptions.recordedFlags = []*config.Install_Flag{{Name: "linkerd-cni-enabled", Value: "true"}}
cniEnabledOptions.cniEnabled = true
cniEnabledValues, _, _ := cniEnabledOptions.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(cniEnabledValues)
withProxyIgnoresOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withProxyIgnoresOptions.ignoreInboundPorts = []string{"22", "8100-8102"}
withProxyIgnoresOptions.ignoreOutboundPorts = []string{"5432"}
withProxyIgnoresValues, _, _ := withProxyIgnoresOptions.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(withProxyIgnoresValues)
withHeartBeatDisabled, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withHeartBeatDisabled.disableHeartbeat = true
withHeartBeatDisabledValues, _, _ := withHeartBeatDisabled.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(withHeartBeatDisabledValues)
withRestrictedDashboardPrivileges, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withRestrictedDashboardPrivileges.restrictDashboardPrivileges = true
withRestrictedDashboardPrivilegesValues, _, _ := withRestrictedDashboardPrivileges.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(withRestrictedDashboardPrivilegesValues)
withControlPlaneTracing, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withControlPlaneTracing.controlPlaneTracing = true
withControlPlaneTracingValues, _, _ := withControlPlaneTracing.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(withControlPlaneTracingValues)
customRegistryOverride := "my.custom.registry/linkerd-io"
withCustomRegistryOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withCustomRegistryOptions.dockerRegistry = customRegistryOverride
withCustomRegistryOptions.recordedFlags = []*config.Install_Flag{
{Name: "registry", Value: customRegistryOverride},
}
withCustomRegistryValues, _, _ := withCustomRegistryOptions.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(withCustomRegistryValues)
withAddOnConfigStage, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withAddOnConfigStageValues, _, _ := withAddOnConfigStage.validateAndBuild(ctx, configStage, nil)
withAddOnConfigStageValues.Tracing["enabled"] = true
addFakeTLSSecrets(withAddOnConfigStageValues)
withAddOnControlPlaneStage, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withAddOnControlPlaneStageValues, _, _ := withAddOnControlPlaneStage.validateAndBuild(ctx, controlPlaneStage, nil)
withAddOnControlPlaneStageValues.Tracing["enabled"] = true
addFakeTLSSecrets(withAddOnControlPlaneStageValues)
withCustomDestinationGetNets, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withCustomDestinationGetNets.destinationGetNetworks = []string{"10.0.0.0/8", "172.0.0.0/8"}
withCustomDestinationGetNetsValues, _, _ := withCustomDestinationGetNets.validateAndBuild(ctx, "", nil)
addFakeTLSSecrets(withCustomDestinationGetNetsValues)
testCases := []struct {
values *charts.Values
goldenFileName string
}{
{defaultValues, "install_default.golden"},
{configValues, "install_config.golden"},
{controlPlaneValues, "install_control-plane.golden"},
{metaValues, "install_output.golden"},
{haValues, "install_ha_output.golden"},
{haWithOverridesValues, "install_ha_with_overrides_output.golden"},
{cniEnabledValues, "install_no_init_container.golden"},
{withProxyIgnoresValues, "install_proxy_ignores.golden"},
{withHeartBeatDisabledValues, "install_heartbeat_disabled_output.golden"},
{withRestrictedDashboardPrivilegesValues, "install_restricted_dashboard.golden"},
{withControlPlaneTracingValues, "install_controlplane_tracing_output.golden"},
{withCustomRegistryValues, "install_custom_registry.golden"},
{withAddOnConfigStageValues, "install_addon_config.golden"},
{withAddOnControlPlaneStageValues, "install_addon_control-plane.golden"},
{withCustomDestinationGetNetsValues, "install_default_override_dst_get_nets.golden"},
}
for i, tc := range testCases {
tc := tc // pin
t.Run(fmt.Sprintf("%d: %s", i, tc.goldenFileName), func(t *testing.T) {
var buf bytes.Buffer
if err := render(&buf, tc.values); err != nil {
t.Fatalf("Failed to render templates: %v", err)
}
diffTestdata(t, tc.goldenFileName, buf.String())
})
}
}
func TestValidateAndBuild_Errors(t *testing.T) {
ctx := context.Background()
t.Run("Fails validation for invalid ignoreInboundPorts", func(t *testing.T) {
installOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
installOptions.ignoreInboundPorts = []string{"-25"}
_, _, err = installOptions.validateAndBuild(ctx, "", nil)
if err == nil {
t.Fatal("expected error but got nothing")
}
})
t.Run("Fails validation for invalid ignoreOutboundPorts", func(t *testing.T) {
installOptions, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
installOptions.ignoreOutboundPorts = []string{"-25"}
_, _, err = installOptions.validateAndBuild(ctx, "", nil)
if err == nil {
t.Fatal("expected error but got nothing")
}
})
}
func testInstallOptions() (*installOptions, error) {
o, err := newInstallOptionsWithDefaults()
if err != nil {
return nil, err
}
o.ignoreCluster = true
o.proxyVersion = installProxyVersion
o.debugImageVersion = installDebugVersion
o.controlPlaneVersion = installControlPlaneVersion
o.heartbeatSchedule = fakeHeartbeatSchedule
o.identityOptions.crtPEMFile = filepath.Join("testdata", "valid-crt.pem")
o.identityOptions.keyPEMFile = filepath.Join("testdata", "valid-key.pem")
o.identityOptions.trustPEMFile = filepath.Join("testdata", "valid-trust-anchors.pem")
return o, nil
}
func TestValidate(t *testing.T) {
t.Run("Accepts the default options as valid", func(t *testing.T) {
opts, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
if err := opts.validate(); err != nil {
t.Fatalf("Unexpected error: %s", err)
}
})
t.Run("Rejects invalid destination networks", func(t *testing.T) {
options, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
options.destinationGetNetworks = []string{"wrong"}
expected := "cannot parse destination get networks: invalid CIDR address: wrong"
err = options.validate()
if err == nil {
t.Fatal("Expected error, got nothing")
}
if err.Error() != expected {
t.Fatalf("Expected error string\"%s\", got \"%s\"", expected, err)
}
})
t.Run("Rejects invalid controller log level", func(t *testing.T) {
options, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
options.controllerLogLevel = "super"
expected := "--controller-log-level must be one of: panic, fatal, error, warn, info, debug"
err = options.validate()
if err == nil {
t.Fatal("Expected error, got nothing")
}
if err.Error() != expected {
t.Fatalf("Expected error string\"%s\", got \"%s\"", expected, err)
}
})
t.Run("Properly validates proxy log level", func(t *testing.T) {
testCases := []struct {
input string
valid bool
}{
{"", false},
{"info", true},
{"somemodule", true},
{"bad%name", false},
{"linkerd2_proxy=debug", true},
{"linkerd2%proxy=debug", false},
{"linkerd2_proxy=foobar", false},
{"linker2d_proxy,std::option", true},
{"warn,linkerd=info", true},
{"warn,linkerd=foobar", false},
}
options, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
for _, tc := range testCases {
options.proxyLogLevel = tc.input
err := options.validate()
if tc.valid && err != nil {
t.Fatalf("Error not expected: %s", err)
}
if !tc.valid && err == nil {
t.Fatalf("Expected error string \"%s is not a valid proxy log level\", got nothing", tc.input)
}
expectedErr := fmt.Sprintf("\"%s\" is not a valid proxy log level - for allowed syntax check https://docs.rs/env_logger/0.6.0/env_logger/#enabling-logging", tc.input)
if tc.input == "" {
expectedErr = "--proxy-log-level must not be empty"
}
if !tc.valid && err.Error() != expectedErr {
t.Fatalf("Expected error string \"%s\", got \"%s\"; input=\"%s\"", expectedErr, err, tc.input)
}
}
})
t.Run("Validates the issuer certs upon install", func(t *testing.T) {
testCases := []struct {
crtFilePrefix string
expectedError string
}{
{"valid", ""},
{"expired", "failed to verify issuer certs stored on disk: not valid anymore. Expired on 1990-01-01T01:01:11Z"},
{"not-valid-yet", "failed to verify issuer certs stored on disk: not valid before: 2100-01-01T01:00:51Z"},
{"wrong-domain", "failed to verify issuer certs stored on disk: x509: certificate is valid for wrong.linkerd.cluster.local, not identity.linkerd.cluster.local"},
{"wrong-algo", "failed to verify issuer certs stored on disk: must use P-256 curve for public key, instead P-521 was used"},
}
for _, tc := range testCases {
options, err := testInstallOptions()
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
options.identityOptions.crtPEMFile = filepath.Join("testdata", tc.crtFilePrefix+"-crt.pem")
options.identityOptions.keyPEMFile = filepath.Join("testdata", tc.crtFilePrefix+"-key.pem")
options.identityOptions.trustPEMFile = filepath.Join("testdata", tc.crtFilePrefix+"-trust-anchors.pem")
_, err = options.identityOptions.validateAndBuild(context.Background())
if tc.expectedError != "" {
if err == nil {
t.Fatal("Expected error, got nothing")
}
if err.Error() != tc.expectedError {
t.Fatalf("Expected error string\"%s\", got \"%s\"", tc.expectedError, err)
}
} else {
if err != nil {
t.Fatalf("Expected no error bu got \"%s\"", err)
}
}
}
})
t.Run("Rejects identity cert files data when external issuer is set", func(t *testing.T) {
options, err := testInstallOptions()
options.identityOptions.crtPEMFile = ""
options.identityOptions.keyPEMFile = ""
options.identityOptions.trustPEMFile = ""
if err != nil {
t.Fatalf("Unexpected error: %v\n", err)
}
withoutCertDataOptions := options.identityOptions
withoutCertDataOptions.identityExternalIssuer = true
withCrtFile := *withoutCertDataOptions
withCrtFile.crtPEMFile = "crt-file"
withTrustAnchorsFile := *withoutCertDataOptions
withTrustAnchorsFile.trustPEMFile = "ta-file"
withKeyFile := *withoutCertDataOptions
withKeyFile.keyPEMFile = "key-file"
testCases := []struct {
input *installIdentityOptions
expectedError string
}{
{withoutCertDataOptions, ""},
{&withCrtFile, "--identity-issuer-certificate-file must not be specified if --identity-external-issuer=true"},
{&withTrustAnchorsFile, "--identity-trust-anchors-file must not be specified if --identity-external-issuer=true"},
{&withKeyFile, "--identity-issuer-key-file must not be specified if --identity-external-issuer=true"},
}
for _, tc := range testCases {
err = tc.input.validate()
if tc.expectedError != "" {
if err == nil {
t.Fatal("Expected error, got nothing")
}
if err.Error() != tc.expectedError {
t.Fatalf("Expected error string\"%s\", got \"%s\"", tc.expectedError, err)
}
} else {
if err != nil {
t.Fatalf("Expected no error bu got \"%s\"", err)
}
}
}
})
}
func fakeHeartbeatSchedule() string {
return "1 2 3 4 5"
}
func addFakeTLSSecrets(values *charts.Values) {
values.ProxyInjector.CrtPEM = "proxy injector crt"
values.ProxyInjector.KeyPEM = "proxy injector key"
values.ProxyInjector.CaBundle = "proxy injector CA bundle"
values.ProfileValidator.CrtPEM = "profile validator crt"
values.ProfileValidator.KeyPEM = "profile validator key"
values.ProfileValidator.CaBundle = "profile validator CA bundle"
values.Tap.CrtPEM = "tap crt"
values.Tap.KeyPEM = "tap key"
values.Tap.CaBundle = "tap CA bundle"
}