Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ web/app/yarn-error.log
vendor
**/*.swp
**/charts/**/charts
go-test.json
package-lock.json
.vscode
**/coverage*
12 changes: 12 additions & 0 deletions cli/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,18 @@ func installControlPlane(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Wr
}
}

// in order to correctly initialize the issuer credentials the overrides
// (from above) need to be set/applied to the values themselves
// specifically identity issuer scheme, and trust values
data, err := yaml.Marshal(valuesOverrides)
if err != nil {
return err
}
err = yaml.Unmarshal(data, values)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relies on the "partial unmarshal" behavior where existing fields in values will be retained with their value if the field is not present in data. This is a bit subtle and in a casual reading of this code, one might expect values to be completely replaced by data. I think this could use a comment.

if err != nil {
return err
}

err = initializeIssuerCredentials(ctx, k8sAPI, values)
if err != nil {
return err
Expand Down
214 changes: 214 additions & 0 deletions cli/cmd/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"testing"

"github.com/go-openapi/testify/v2/assert"
"github.com/linkerd/linkerd2/cli/flag"
charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/tls"
"helm.sh/helm/v3/pkg/cli/values"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
Expand Down Expand Up @@ -314,6 +317,213 @@ func TestRender(t *testing.T) {
}
}

// TestOverrideIssuer calls install control plane with the goal of testing
// options overrides for initialize issuer credentials.
func TestOverrideIssuer(t *testing.T) {
removeIssuerCrt := func() (*charts.Values, error) {
t.Helper()
values, err := testInstallOptionsFakeCerts()
if err != nil {
return nil, err
}
values.Identity.Issuer.TLS.CrtPEM = ""
return values, nil
}
removeIssuerKey := func() (*charts.Values, error) {
t.Helper()
values, err := testInstallOptionsFakeCerts()
if err != nil {
return nil, err
}
values.Identity.Issuer.TLS.KeyPEM = ""
return values, nil
}
removeTrustAnchor := func() (*charts.Values, error) {
t.Helper()
values, err := testInstallOptionsFakeCerts()
if err != nil {
return nil, err
}
values.IdentityTrustAnchorsPEM = ""
return values, nil
}
assert := assert.New(t)
read := func(filename string) []byte {
t.Helper()
data, err := os.ReadFile(path.Join("testdata", filename))
if assert.NoError(err, "cannot read-file filename=%s", filename) {
return data
}
return nil
}
// newK8S returns a test implementation of the k8s API; after setting the
// issuer trust anchor and tls crt+key as a secret.
newK8S := func(opts values.Options) *k8s.KubernetesAPI {
t.Helper()
buf := &bytes.Buffer{}
err := renderCRDs(context.Background(), nil, buf, opts, "yaml")
assert.NoError(err, "cannot render-crds for new-k8s-api opts=%+v", opts)
api, err := k8s.NewFakeAPIFromManifests([]io.Reader{buf})
Comment on lines +361 to +367
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to account for the CRDs here. Shorter version:

Suggested change
buf := &bytes.Buffer{}
err := renderCRDs(context.Background(), nil, buf, opts, "yaml")
if err != nil {
t.Fatalf("cannot render-crds for new-k8s-api opts=%+v err=%v",
opts, err)
}
api, err := k8s.NewFakeAPIFromManifests([]io.Reader{buf})
api, err := k8s.NewFakeAPI()

if assert.NoError(err, "cannot create k8s api from manifests") {
_, err = api.CoreV1().Secrets(controlPlaneNamespace).Create(context.Background(),
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: k8s.IdentityIssuerSecretName,
Namespace: controlPlaneNamespace,
},
Data: map[string][]byte{
k8s.IdentityIssuerTrustAnchorsNameExternal: read("valid-trust-anchors.pem"),
corev1.TLSCertKey: read("valid-crt.pem"),
corev1.TLSPrivateKeyKey: read("valid-key.pem"),
}}, metav1.CreateOptions{})
if assert.NoError(err, "cannot create secrets for new-k8s-api") {
return api
}
}
return nil
}
controlPlaneNamespace = defaultLinkerdNamespace
for i, test := range []struct {
options values.Options
values func() (*charts.Values, error)
k8sAPI *k8s.KubernetesAPI
expErr string
expIdentityTrustAnchor bool
expIssuerCrt bool
expIssuerKey bool
expIssuerName string
}{
{
// no options; no certs in values -> generated anchor; key + crt
options: values.Options{},
values: testInstallValuesNoCertsNoHA,
k8sAPI: nil,
expIdentityTrustAnchor: true,
expIssuerKey: true,
expIssuerCrt: true,
expIssuerName: fmt.Sprintf("identity.%s.%s",
controlPlaneNamespace, "test-override-issuer"),
},
{
// no options; fake certs in values -> fake certs untouched
options: values.Options{},
values: testInstallOptionsFakeCerts,
k8sAPI: nil,
expIdentityTrustAnchor: true,
expIssuerKey: true,
expIssuerCrt: true,
expIssuerName: "identity.linkerd.cluster.local",
},
{
// issuer scheme in options; no certs in values; nil k8s api ->
// error trying to call k8s
options: values.Options{
Values: []string{"identity.issuer.scheme=kubernetes.io/tls"},
},
values: testInstallValuesNoCertsNoHA,
k8sAPI: nil,
expErr: "--ignore-cluster is not supported when --identity-external-issuer=true",
expIdentityTrustAnchor: false,
expIssuerKey: false,
expIssuerCrt: false,
expIssuerName: "",
},
{
// issuer scheme in options; no certs in values; fake k8s api ->
// trust anchor is set
options: values.Options{
Values: []string{"identity.issuer.scheme=kubernetes.io/tls"},
},
values: testInstallValuesNoCertsNoHA,
k8sAPI: newK8S(values.Options{}),
expErr: "",
expIdentityTrustAnchor: true,
expIssuerKey: false,
expIssuerCrt: false,
expIssuerName: "identity.linkerd.cluster.local",
},
{
// no options; fake certs in values; remove trust anchor -> err
options: values.Options{},
values: removeTrustAnchor,
k8sAPI: nil,
expErr: "a trust anchors file must be specified if other credentials are provided",
expIdentityTrustAnchor: false,
expIssuerCrt: true,
expIssuerKey: true,
expIssuerName: "identity.linkerd.cluster.local",
},
{
// no options; fake certs in values; remove issuer crt -> err
options: values.Options{},
values: removeIssuerCrt,
k8sAPI: nil,
expErr: "a certificate file must be specified if other credentials are provided",
expIdentityTrustAnchor: true,
expIssuerCrt: false,
expIssuerName: "identity.linkerd.cluster.local",
expIssuerKey: true,
},
{
// no options; fake certs in values; remove issuer key -> err
options: values.Options{},
values: removeIssuerKey,
k8sAPI: nil,
expErr: "a private key file must be specified if other credentials are provided",
expIdentityTrustAnchor: true,
expIssuerCrt: true,
expIssuerName: "identity.linkerd.cluster.local",
expIssuerKey: false,
},
} {
values, err := test.values()
assert.NoError(err, "%02d/test install options failed with an error", i)
values.IdentityTrustDomain = "test-override-issuer"
// ensure the install options created above meet expectations (we are
// testing the override not the values)
assert.Equal(k8s.IdentityIssuerSchemeLinkerd, values.Identity.Issuer.Scheme)
var buf bytes.Buffer
err = installControlPlane(context.Background(), test.k8sAPI, &buf, values, nil, test.options, "yaml")
if test.expErr != "" {
assert.EqualError(err, test.expErr, "%02d/install control plane returned incorrect error", i)
} else {
assert.NoError(err, "%02d/install control plane failed with an error", i)
}
if test.expIdentityTrustAnchor {
assert.NotEmpty(t, values.IdentityTrustAnchorsPEM, "%02d/identity trust anchor is not set", i)
crt, err := tls.DecodePEMCrt(values.IdentityTrustAnchorsPEM)
assert.NoError(err, "%02d/generated identity-trust-anchors-pem cannot be decoded", i)
assert.NotNil(crt, "%02d/generated identity-trust-anchors-pem cannot be decoded (nil)", i)
assert.NotNil(crt.Certificate, "%02d/generated identity-trust-anchors-pem certificate is invalid", i)
assert.Equal(
test.expIssuerName,
crt.Certificate.Issuer.CommonName,
"%02/generated identity-trust-anchors-pem certificate common-name is incorrect", i)
} else {
assert.Empty(values.IdentityTrustAnchorsPEM, "%02d/identity was incorrectly set", i)
}
if test.expIssuerCrt {
assert.NotEmpty(values.Identity.Issuer.TLS.CrtPEM, "%02d/identity issuer crt is not set", i)
assert.NotEmpty(values.Identity.Issuer.TLS.CrtPEM, "%02d/generated identity-issuer-tls-crt-pem is empty", i)
crt, err := tls.DecodePEMCrt(values.Identity.Issuer.TLS.CrtPEM)
assert.NoError(err, "%02d/generated identity-issuer-tls-crt-pem cannot be decoded", i)
assert.NotNil(crt, "%02d/generated identity-issuer-tls-crt-pem cannot be decoded (nil)", i)
assert.NotNil(crt.Certificate, "%02d/generated identity-issuer-tls-crt-pem certificate is invalid", i)
} else {
assert.Empty(values.Identity.Issuer.TLS.CrtPEM, "%02d/identity issuer crt was incorrectly set", i)
}
if test.expIssuerKey {
assert.NotEmpty(values.Identity.Issuer.TLS.KeyPEM, "%02d/identity issuer tls key is not set", i)
assert.NotEmpty(values.Identity.Issuer.TLS.KeyPEM, "%02d/generated identity-issuer-tls-key-pem is empty", i)
key, err := tls.DecodePEMKey(values.Identity.Issuer.TLS.KeyPEM)
assert.NoError(err, "%02d/generated identity-issuer-tls-key-pem cannot be decoded", i)
assert.NotNil(key, "%02d/generated identity-issuer-tls-key-pem cannot be decoded (nil)", i)
} else {
assert.Empty(values.Identity.Issuer.TLS.KeyPEM, "%02d/identity issuer tls key was incorrectly set", i)
}
}
}

func TestIgnoreCluster(t *testing.T) {
defaultValues, err := testInstallOptions()
if err != nil {
Expand Down Expand Up @@ -550,6 +760,10 @@ func testInstallOptionsNoCerts(ha bool) (*charts.Values, error) {
return values, nil
}

func testInstallValuesNoCertsNoHA() (*charts.Values, error) {
return testInstallOptionsNoCerts(false)
}

func testInstallValues() (*charts.Values, error) {
values, err := charts.NewValues()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/fatih/color v1.19.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-openapi/spec v0.22.4
github.com/go-openapi/testify/v2 v2.4.0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hesitate to introduce a new testing library that is used here but not throughout the project. I'd prefer to evaluate testify independently of this PR. Can the test here be written without it?

github.com/go-test/deep v1.1.1
github.com/golang/protobuf v1.5.4
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ go-lint *flags:
golangci-lint run {{ flags }}

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

##
## Rust
Expand Down
Loading