Skip to content

Commit f039635

Browse files
authored
fix(CLI): Apply overrides to chart values on install #15025 (#15181)
Running `linkerd upgrade` fails after running install with an external ca. The issue presents at upgrade but the cause is due to install incorrectly initializing the chart values for identity, specifically it initializes the issuer versus querying for one that is already set. ```bash linkerd install \ --ha \ --set proxy.metrics.hostnameLabels=true \ --set identity.externalCA=true \ --set identity.issuer.scheme=kubernetes.io/tls \ --set policyValidator.externalSecret=true \ --set policyValidator.injectCaFrom=cert-manager/webhook-ca \ --set proxyInjector.externalSecret=true \ --set proxyInjector.injectCaFrom=cert-manager/webhook-ca \ --set profileValidator.externalSecret=true \ --set profileValidator.injectCaFrom=cert-manager/webhook-ca \ --ignore-cluster ``` The install process did not apply the '--set' command line options onto internal values state before attempting to initialize the issuer credentials. * The change applies the overrides to the values before initialize. * Add test code to cover initializeIssuerCredentials. * Track unit test run history in 'go-test.json' in the root directory when running in the justfile. * Ignore 'go-test.json.' * Add testify assertions as a go module. Added unit test and ran through the integration test suite. Fixes #15025
1 parent 9e25fc5 commit f039635

4 files changed

Lines changed: 257 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ web/app/yarn-error.log
1414
vendor
1515
**/*.swp
1616
**/charts/**/charts
17+
go-test.json
1718
package-lock.json
1819
.vscode
1920
**/coverage*

cli/cmd/install.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,21 @@ func installControlPlane(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Wr
303303
}
304304
}
305305

306+
// in order to correctly initialize the issuer credentials the overrides
307+
// (from above) need to be set/applied to the values themselves
308+
// specifically identity issuer scheme, and trust values
309+
//
310+
// marshal+unmarshal here will only apply specific overrides to values and
311+
// will not wipe out values that are not set.
312+
data, err := yaml.Marshal(valuesOverrides)
313+
if err != nil {
314+
return err
315+
}
316+
err = yaml.Unmarshal(data, values)
317+
if err != nil {
318+
return err
319+
}
320+
306321
err = initializeIssuerCredentials(ctx, k8sAPI, values)
307322
if err != nil {
308323
return err

cli/cmd/install_test.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"os"
9+
"path"
910
"path/filepath"
1011
"strings"
1112
"testing"
@@ -16,6 +17,7 @@ import (
1617
"github.com/linkerd/linkerd2/pkg/tls"
1718
"helm.sh/helm/v3/pkg/cli/values"
1819
corev1 "k8s.io/api/core/v1"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1921
)
2022

2123
const (
@@ -314,6 +316,240 @@ func TestRender(t *testing.T) {
314316
}
315317
}
316318

319+
// TestOverrideIssuer calls install control plane with the goal of testing
320+
// options overrides for initialize issuer credentials.
321+
func TestOverrideIssuer(t *testing.T) {
322+
removeIssuerCrt := func() (*charts.Values, error) {
323+
t.Helper()
324+
values, err := testInstallOptionsFakeCerts()
325+
if err != nil {
326+
return nil, err
327+
}
328+
values.Identity.Issuer.TLS.CrtPEM = ""
329+
return values, nil
330+
}
331+
removeIssuerKey := func() (*charts.Values, error) {
332+
t.Helper()
333+
values, err := testInstallOptionsFakeCerts()
334+
if err != nil {
335+
return nil, err
336+
}
337+
values.Identity.Issuer.TLS.KeyPEM = ""
338+
return values, nil
339+
}
340+
removeTrustAnchor := func() (*charts.Values, error) {
341+
t.Helper()
342+
values, err := testInstallOptionsFakeCerts()
343+
if err != nil {
344+
return nil, err
345+
}
346+
values.IdentityTrustAnchorsPEM = ""
347+
return values, nil
348+
}
349+
read := func(filename string) []byte {
350+
t.Helper()
351+
data, err := os.ReadFile(path.Join("testdata", filename))
352+
if err != nil {
353+
t.Fatalf("cannot read filename=%s err=%v", filename, err)
354+
}
355+
return data
356+
}
357+
// newK8S returns a test implementation of the k8s API; after setting the
358+
// issuer trust anchor and tls crt+key as a secret.
359+
newK8S := func() *k8s.KubernetesAPI {
360+
t.Helper()
361+
api, err := k8s.NewFakeAPI()
362+
if err != nil {
363+
t.Fatalf("cannot create new fake-api from manifests err=%v", err)
364+
}
365+
_, err = api.CoreV1().Secrets(controlPlaneNamespace).Create(context.Background(),
366+
&corev1.Secret{
367+
ObjectMeta: metav1.ObjectMeta{
368+
Name: k8s.IdentityIssuerSecretName,
369+
Namespace: controlPlaneNamespace,
370+
},
371+
Data: map[string][]byte{
372+
k8s.IdentityIssuerTrustAnchorsNameExternal: read("valid-trust-anchors.pem"),
373+
corev1.TLSCertKey: read("valid-crt.pem"),
374+
corev1.TLSPrivateKeyKey: read("valid-key.pem"),
375+
}}, metav1.CreateOptions{})
376+
if err != nil {
377+
t.Fatalf("cannot create secret for new-k8s-api err=%v", err)
378+
}
379+
return api
380+
}
381+
controlPlaneNamespace = defaultLinkerdNamespace
382+
for i, test := range []struct {
383+
options values.Options
384+
values func() (*charts.Values, error)
385+
k8sAPI *k8s.KubernetesAPI
386+
expErr string
387+
expIdentityTrustAnchor bool
388+
expIssuerCrt bool
389+
expIssuerKey bool
390+
expIssuerName string
391+
}{
392+
{
393+
// no options; no certs in values -> generated anchor; key + crt
394+
options: values.Options{},
395+
values: testInstallValuesNoCertsNoHA,
396+
k8sAPI: nil,
397+
expIdentityTrustAnchor: true,
398+
expIssuerKey: true,
399+
expIssuerCrt: true,
400+
expIssuerName: fmt.Sprintf("identity.%s.%s",
401+
controlPlaneNamespace, "test-override-issuer"),
402+
},
403+
{
404+
// no options; fake certs in values -> fake certs untouched
405+
options: values.Options{},
406+
values: testInstallOptionsFakeCerts,
407+
k8sAPI: nil,
408+
expIdentityTrustAnchor: true,
409+
expIssuerKey: true,
410+
expIssuerCrt: true,
411+
expIssuerName: "identity.linkerd.cluster.local",
412+
},
413+
{
414+
// issuer scheme in options; no certs in values; nil k8s api ->
415+
// error trying to call k8s
416+
options: values.Options{
417+
Values: []string{"identity.issuer.scheme=kubernetes.io/tls"},
418+
},
419+
values: testInstallValuesNoCertsNoHA,
420+
k8sAPI: nil,
421+
expErr: "--ignore-cluster is not supported when --identity-external-issuer=true",
422+
expIdentityTrustAnchor: false,
423+
expIssuerKey: false,
424+
expIssuerCrt: false,
425+
expIssuerName: "",
426+
},
427+
{
428+
// issuer scheme in options; no certs in values; fake k8s api ->
429+
// trust anchor is set
430+
options: values.Options{
431+
Values: []string{"identity.issuer.scheme=kubernetes.io/tls"},
432+
},
433+
values: testInstallValuesNoCertsNoHA,
434+
k8sAPI: newK8S(),
435+
expErr: "",
436+
expIdentityTrustAnchor: true,
437+
expIssuerKey: false,
438+
expIssuerCrt: false,
439+
expIssuerName: "identity.linkerd.cluster.local",
440+
},
441+
{
442+
// no options; fake certs in values; remove trust anchor -> err
443+
options: values.Options{},
444+
values: removeTrustAnchor,
445+
k8sAPI: nil,
446+
expErr: "a trust anchors file must be specified if other credentials are provided",
447+
expIdentityTrustAnchor: false,
448+
expIssuerCrt: true,
449+
expIssuerKey: true,
450+
expIssuerName: "identity.linkerd.cluster.local",
451+
},
452+
{
453+
// no options; fake certs in values; remove issuer crt -> err
454+
options: values.Options{},
455+
values: removeIssuerCrt,
456+
k8sAPI: nil,
457+
expErr: "a certificate file must be specified if other credentials are provided",
458+
expIdentityTrustAnchor: true,
459+
expIssuerCrt: false,
460+
expIssuerName: "identity.linkerd.cluster.local",
461+
expIssuerKey: true,
462+
},
463+
{
464+
// no options; fake certs in values; remove issuer key -> err
465+
options: values.Options{},
466+
values: removeIssuerKey,
467+
k8sAPI: nil,
468+
expErr: "a private key file must be specified if other credentials are provided",
469+
expIdentityTrustAnchor: true,
470+
expIssuerCrt: true,
471+
expIssuerName: "identity.linkerd.cluster.local",
472+
expIssuerKey: false,
473+
},
474+
} {
475+
values, err := test.values()
476+
if err != nil {
477+
t.Fatalf("%02d/test install options failed with an error err=%v", i, err)
478+
}
479+
values.IdentityTrustDomain = "test-override-issuer"
480+
// ensure the install options created above meet expectations (we are
481+
// testing the override not the values)
482+
if values.Identity.Issuer.Scheme != k8s.IdentityIssuerSchemeLinkerd {
483+
t.Fatalf("%02d/identity issuer scheme is incorrect: %s != %s", i,
484+
k8s.IdentityIssuerSchemeLinkerd, values.Identity.Issuer.Scheme)
485+
}
486+
var buf bytes.Buffer
487+
err = installControlPlane(context.Background(), test.k8sAPI, &buf, values, nil, test.options, "yaml")
488+
if test.expErr != "" {
489+
if test.expErr != err.Error() {
490+
t.Fatalf("%02d/install control plane returned incorrect error %s<>%v", i, test.expErr, err)
491+
}
492+
} else {
493+
if err != nil {
494+
t.Fatalf("%02d/install control plane failed with an error=%v", i, err)
495+
}
496+
}
497+
if test.expIdentityTrustAnchor {
498+
if values.IdentityTrustAnchorsPEM == "" {
499+
t.Fatalf("%02d/identity trust-anchors-pem is empty", i)
500+
}
501+
crt, err := tls.DecodePEMCrt(values.IdentityTrustAnchorsPEM)
502+
if err != nil {
503+
t.Fatalf("%02d/generated identity-trust-anchors-pem cannot be decoded", i)
504+
}
505+
if crt == nil {
506+
t.Fatalf("%02d/generated identity-trust-anchors-pem cannot be decoded (nil)", i)
507+
}
508+
if crt.Certificate == nil {
509+
t.Fatalf("%02d/generated identity-trust-anchors-pem certificate is invalid", i)
510+
}
511+
if test.expIssuerName != crt.Certificate.Issuer.CommonName {
512+
t.Fatalf("%02d/generated identity-trust-anchors-pem certificate common-name is incorrect %s<>%s", i,
513+
test.expIssuerName,
514+
crt.Certificate.Issuer.CommonName)
515+
}
516+
} else if values.IdentityTrustAnchorsPEM != "" {
517+
t.Fatalf("%02d/identity was incorrectly set pem=%s", i, values.IdentityTrustAnchorsPEM)
518+
}
519+
if test.expIssuerCrt {
520+
if values.Identity.Issuer.TLS.CrtPEM == "" {
521+
t.Fatalf("%02d/generated identity-issuer-tls-crt-pem is empty", i)
522+
}
523+
crt, err := tls.DecodePEMCrt(values.Identity.Issuer.TLS.CrtPEM)
524+
if err != nil {
525+
t.Fatalf("%02d/generated identity-issuer-tls-crt-pem cannot be decoded err=%v", i, err)
526+
}
527+
if crt == nil {
528+
t.Fatalf("%02d/generated identity-issuer-tls-crt-pem cannot be decoded (nil)", i)
529+
}
530+
if crt.Certificate == nil {
531+
t.Fatalf("%02d/generated identity-issuer-tls-crt-pem certificate is invalid (nil)", i)
532+
}
533+
} else if values.Identity.Issuer.TLS.CrtPEM != "" {
534+
t.Fatalf("%02d/identity issuer crt was incorrectly set pem=%s", i, values.Identity.Issuer.TLS.CrtPEM)
535+
}
536+
if test.expIssuerKey {
537+
if values.Identity.Issuer.TLS.KeyPEM == "" {
538+
t.Fatalf("%02d/generated identity-issuer-tls-key-pem is empty", i)
539+
}
540+
key, err := tls.DecodePEMKey(values.Identity.Issuer.TLS.KeyPEM)
541+
if err != nil {
542+
t.Fatalf("%02d/generated identity-issuer-tls-key-pem cannot be decoded err=%v", i, err)
543+
}
544+
if key == nil {
545+
t.Fatalf("%02d/generated identity-issuer-tls-key-pem cannot be decoded (nil)", i)
546+
}
547+
} else if values.Identity.Issuer.TLS.KeyPEM != "" {
548+
t.Fatalf("%02d/identity issuer tls key was incorrectly set pem=%s", i, values.Identity.Issuer.TLS.KeyPEM)
549+
}
550+
}
551+
}
552+
317553
func TestIgnoreCluster(t *testing.T) {
318554
defaultValues, err := testInstallOptions()
319555
if err != nil {
@@ -550,6 +786,10 @@ func testInstallOptionsNoCerts(ha bool) (*charts.Values, error) {
550786
return values, nil
551787
}
552788

789+
func testInstallValuesNoCertsNoHA() (*charts.Values, error) {
790+
return testInstallOptionsNoCerts(false)
791+
}
792+
553793
func testInstallValues() (*charts.Values, error) {
554794
values, err := charts.NewValues()
555795
if err != nil {

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ go-lint *flags:
2020
golangci-lint run {{ flags }}
2121

2222
go-test:
23-
LINKERD_TEST_PRETTY_DIFF=1 gotestsum -- -race -v -mod=readonly --timeout 10m ./...
23+
LINKERD_TEST_PRETTY_DIFF=1 gotestsum --jsonfile go-test.json -- -race -v -mod=readonly --timeout 10m ./...
2424

2525
##
2626
## Rust

0 commit comments

Comments
 (0)