From 36bc6791428370bf2edcea0cf8a161333e6dda35 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Fri, 5 Mar 2021 15:52:17 -0600 Subject: [PATCH] Add diagnostic logging to "pinniped get kubeconfig". These stderr logs should help clarify all the autodetection logic that's happening in a particular run. Signed-off-by: Matt Moyer --- cmd/pinniped/cmd/kubeconfig.go | 41 ++++++++--- cmd/pinniped/cmd/kubeconfig_test.go | 103 +++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 6a4e8327..3314e7a0 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -10,20 +10,22 @@ import ( "fmt" "io" "io/ioutil" + "log" "os" "strconv" "strings" "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-logr/logr" + "github.com/go-logr/stdr" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + _ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - _ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go - conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" @@ -34,6 +36,7 @@ import ( type kubeconfigDeps struct { getPathToSelf func() (string, error) getClientset func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) + log logr.Logger } func kubeconfigRealDeps() kubeconfigDeps { @@ -53,6 +56,7 @@ func kubeconfigRealDeps() kubeconfigDeps { } return client.PinnipedConcierge, nil }, + log: stdr.New(log.New(os.Stderr, "", 0)), } } @@ -181,7 +185,7 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar } if !flags.concierge.disabled { - credentialIssuer, err := lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer) + credentialIssuer, err := lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer, deps.log) if err != nil { return err } @@ -190,12 +194,13 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar clientset, flags.concierge.authenticatorType, flags.concierge.authenticatorName, + deps.log, ) if err != nil { return err } - if err := configureConcierge(credentialIssuer, authenticator, &flags, cluster, &oidcCABundle, &execConfig); err != nil { + if err := configureConcierge(credentialIssuer, authenticator, &flags, cluster, &oidcCABundle, &execConfig, deps.log); err != nil { return err } } @@ -246,7 +251,7 @@ func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigPar return writeConfigAsYAML(out, newExecKubeconfig(cluster, &execConfig)) } -func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authenticator metav1.Object, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, oidcCABundle *string, execConfig *clientcmdapi.ExecConfig) error { +func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authenticator metav1.Object, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, oidcCABundle *string, execConfig *clientcmdapi.ExecConfig, log logr.Logger) error { var conciergeCABundleData []byte // Autodiscover the --concierge-mode. @@ -258,9 +263,11 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe } switch strategy.Frontend.Type { case configv1alpha1.TokenCredentialRequestAPIFrontendType: + log.Info("detected Concierge in TokenCredentialRequest API mode") flags.concierge.mode = modeTokenCredentialRequestAPI break strategyLoop case configv1alpha1.ImpersonationProxyFrontendType: + flags.concierge.mode = modeImpersonationProxy flags.concierge.endpoint = strategy.Frontend.ImpersonationProxyInfo.Endpoint var err error @@ -268,6 +275,7 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe if err != nil { return fmt.Errorf("autodiscovered Concierge CA bundle is invalid: %w", err) } + log.Info("detected Concierge in impersonation proxy mode", "endpoint", strategy.Frontend.ImpersonationProxyInfo.Endpoint) break strategyLoop default: // Skip any unknown frontend types. @@ -288,6 +296,7 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe // If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set // them to point at the discovered WebhookAuthenticator. if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" { + log.Info("discovered WebhookAuthenticator", "name", auth.Name) flags.concierge.authenticatorType = "webhook" flags.concierge.authenticatorName = auth.Name } @@ -295,17 +304,20 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe // If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set // them to point at the discovered JWTAuthenticator. if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" { + log.Info("discovered JWTAuthenticator", "name", auth.Name) flags.concierge.authenticatorType = "jwt" flags.concierge.authenticatorName = auth.Name } // If the --oidc-issuer flag was not set explicitly, default it to the spec.issuer field of the JWTAuthenticator. if flags.oidc.issuer == "" { + log.Info("detected OIDC issuer", "issuer", auth.Spec.Issuer) flags.oidc.issuer = auth.Spec.Issuer } // If the --oidc-request-audience flag was not set explicitly, default it to the spec.audience field of the JWTAuthenticator. if flags.oidc.requestAudience == "" { + log.Info("detected OIDC audience", "audience", auth.Spec.Audience) flags.oidc.requestAudience = auth.Spec.Audience } @@ -316,16 +328,19 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe if err != nil { return fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w", auth.Name, err) } + log.Info("detected OIDC CA bundle", "length", len(decoded)) *oidcCABundle = string(decoded) } } if flags.concierge.endpoint == "" { + log.Info("detected concierge endpoint", "endpoint", v1Cluster.Server) flags.concierge.endpoint = v1Cluster.Server } if conciergeCABundleData == nil { if flags.concierge.caBundlePath == "" { + log.Info("detected concierge CA bundle", "length", len(v1Cluster.CertificateAuthorityData)) conciergeCABundleData = v1Cluster.CertificateAuthorityData } else { caBundleString, err := loadCABundlePaths([]string{flags.concierge.caBundlePath}) @@ -349,6 +364,7 @@ func configureConcierge(credentialIssuer *configv1alpha1.CredentialIssuer, authe // If we're in impersonation proxy mode, the main server endpoint for the kubeconfig also needs to point to the proxy if flags.concierge.mode == modeImpersonationProxy { + log.Info("switching kubeconfig cluster to point at impersonation proxy endpoint", "endpoint", flags.concierge.endpoint) v1Cluster.CertificateAuthorityData = conciergeCABundleData v1Cluster.Server = flags.concierge.endpoint } @@ -383,7 +399,7 @@ func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.E } } -func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string) (*configv1alpha1.CredentialIssuer, error) { +func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log logr.Logger) (*configv1alpha1.CredentialIssuer, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) defer cancelFunc() @@ -403,10 +419,13 @@ func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string) if len(results.Items) > 1 { return nil, fmt.Errorf("multiple CredentialIssuers were found, so the --concierge-credential-issuer flag must be specified") } - return &results.Items[0], nil + + result := &results.Items[0] + log.Info("discovered CredentialIssuer", "name", result.Name) + return result, nil } -func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string) (metav1.Object, error) { +func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log logr.Logger) (metav1.Object, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) defer cancelFunc() @@ -444,6 +463,12 @@ func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authN return nil, fmt.Errorf("no authenticators were found") } if len(results) > 1 { + for _, jwtAuth := range jwtAuths.Items { + log.Info("found JWTAuthenticator", "name", jwtAuth.Name) + } + for _, webhook := range webhooks.Items { + log.Info("found WebhookAuthenticator", "name", webhook.Name) + } return nil, fmt.Errorf("multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified") } return results[0], nil diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 79fdb2dd..7d84d31c 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -26,6 +26,7 @@ import ( "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/testlogger" ) func TestGetKubeconfig(t *testing.T) { @@ -46,6 +47,7 @@ func TestGetKubeconfig(t *testing.T) { getClientsetErr error conciergeObjects []runtime.Object conciergeReactions []kubetesting.Reactor + wantLogs []string wantError bool wantStdout string wantStderr string @@ -171,6 +173,9 @@ func TestGetKubeconfig(t *testing.T) { conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: webhookauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found @@ -186,6 +191,9 @@ func TestGetKubeconfig(t *testing.T) { conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: jwtauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found @@ -201,6 +209,9 @@ func TestGetKubeconfig(t *testing.T) { conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt" @@ -214,6 +225,9 @@ func TestGetKubeconfig(t *testing.T) { conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, conciergeReactions: []kubetesting.Reactor{ &kubetesting.SimpleReactor{ Verb: "*", @@ -245,6 +259,9 @@ func TestGetKubeconfig(t *testing.T) { }, }, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: failed to list WebhookAuthenticator objects for autodiscovery: some list error @@ -258,6 +275,9 @@ func TestGetKubeconfig(t *testing.T) { conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: no authenticators were found @@ -275,6 +295,13 @@ func TestGetKubeconfig(t *testing.T) { &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3"}}, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-4"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-1"`, + `"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-2"`, + `"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-3"`, + `"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-4"`, + }, wantError: true, wantStderr: here.Doc(` Error: multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified @@ -289,6 +316,9 @@ func TestGetKubeconfig(t *testing.T) { &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: could not autodiscover --concierge-mode and none was provided @@ -340,6 +370,9 @@ func TestGetKubeconfig(t *testing.T) { }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: autodiscovered Concierge CA bundle is invalid: illegal base64 data at input byte 7 @@ -372,6 +405,13 @@ func TestGetKubeconfig(t *testing.T) { }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="detected Concierge in TokenCredentialRequest API mode"`, + `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, + }, wantError: true, wantStderr: here.Doc(` Error: could not autodiscover --oidc-issuer and none was provided @@ -401,6 +441,12 @@ func TestGetKubeconfig(t *testing.T) { }, }, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected OIDC issuer" "issuer"=""`, + `"level"=0 "msg"="detected OIDC audience" "audience"=""`, + }, wantError: true, wantStderr: here.Doc(` Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7 @@ -428,6 +474,9 @@ func TestGetKubeconfig(t *testing.T) { }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + }, wantError: true, wantStderr: here.Doc(` Error: could not read --concierge-ca-bundle: open ./does/not/exist: no such file or directory @@ -452,6 +501,12 @@ func TestGetKubeconfig(t *testing.T) { }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, + }, wantError: true, wantStderr: here.Doc(` Error: only one of --static-token and --static-token-env can be specified @@ -485,6 +540,12 @@ func TestGetKubeconfig(t *testing.T) { }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, + }, wantStdout: here.Doc(` apiVersion: v1 clusters: @@ -539,6 +600,12 @@ func TestGetKubeconfig(t *testing.T) { }, &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, + }, wantStdout: here.Doc(` apiVersion: v1 clusters: @@ -601,6 +668,15 @@ func TestGetKubeconfig(t *testing.T) { }, }, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected OIDC issuer" "issuer"="https://example.com/issuer"`, + `"level"=0 "msg"="detected OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="detected OIDC CA bundle" "length"=587`, + `"level"=0 "msg"="detected concierge endpoint" "endpoint"="https://fake-server-url-value"`, + `"level"=0 "msg"="detected concierge CA bundle" "length"=37`, + }, wantStdout: here.Docf(` apiVersion: v1 clusters: @@ -645,9 +721,12 @@ func TestGetKubeconfig(t *testing.T) { name: "autodetect nothing, set a bunch of options", args: []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-credential-issuer", "test-credential-issuer", "--concierge-api-group-suffix", "tuna.io", "--concierge-authenticator-type", "webhook", "--concierge-authenticator-name", "test-authenticator", + "--concierge-endpoint", "https://concierge-endpoint.example.com", + "--concierge-ca-bundle", testConciergeCABundlePath, "--oidc-issuer", "https://example.com/issuer", "--oidc-skip-browser", "--oidc-listen-port", "1234", @@ -697,8 +776,8 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-api-group-suffix=tuna.io - --concierge-authenticator-name=test-authenticator - --concierge-authenticator-type=webhook - - --concierge-endpoint=https://fake-server-url-value - - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --concierge-endpoint=https://concierge-endpoint.example.com + - --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E= - --concierge-mode=TokenCredentialRequestAPI - --issuer=https://example.com/issuer - --client-id=pinniped-cli @@ -739,6 +818,14 @@ func TestGetKubeconfig(t *testing.T) { }, }, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected OIDC issuer" "issuer"="https://example.com/issuer"`, + `"level"=0 "msg"="detected OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="detected OIDC CA bundle" "length"=587`, + `"level"=0 "msg"="switching kubeconfig cluster to point at impersonation proxy endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + }, wantStdout: here.Docf(` apiVersion: v1 clusters: @@ -831,6 +918,15 @@ func TestGetKubeconfig(t *testing.T) { }, }, }, + wantLogs: []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="detected Concierge in impersonation proxy mode" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="detected OIDC issuer" "issuer"="https://example.com/issuer"`, + `"level"=0 "msg"="detected OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="detected OIDC CA bundle" "length"=587`, + `"level"=0 "msg"="switching kubeconfig cluster to point at impersonation proxy endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + }, wantStdout: here.Docf(` apiVersion: v1 clusters: @@ -875,6 +971,7 @@ func TestGetKubeconfig(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + testLog := testlogger.New(t) cmd := kubeconfigCommand(kubeconfigDeps{ getPathToSelf: func() (string, error) { if tt.getPathToSelfErr != nil { @@ -897,6 +994,7 @@ func TestGetKubeconfig(t *testing.T) { } return fake, nil }, + log: testLog, }) require.NotNil(t, cmd) @@ -910,6 +1008,7 @@ func TestGetKubeconfig(t *testing.T) { } else { require.NoError(t, err) } + testLog.Expect(tt.wantLogs) require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") })