From 71df5dd140424aae909a7dfc72405bef1c4b66af Mon Sep 17 00:00:00 2001 From: Akash Singhal Date: Wed, 6 Mar 2024 18:00:42 -0800 Subject: [PATCH] feat: add debug logs to k8s secret and docker config auth providers (#1319) --- go.mod | 1 + pkg/common/oras/authprovider/authprovider.go | 16 +- .../authprovider/k8secret_authprovider.go | 29 +- .../k8secret_authprovider_test.go | 302 +++++++++++++++++- 4 files changed, 332 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index e1388f848..a48836f08 100644 --- a/go.mod +++ b/go.mod @@ -90,6 +90,7 @@ require ( github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-ini/ini v1.67.0 // indirect diff --git a/pkg/common/oras/authprovider/authprovider.go b/pkg/common/oras/authprovider/authprovider.go index 053185c81..6d08941ae 100644 --- a/pkg/common/oras/authprovider/authprovider.go +++ b/pkg/common/oras/authprovider/authprovider.go @@ -25,6 +25,7 @@ import ( "time" re "github.com/deislabs/ratify/errors" + "github.com/deislabs/ratify/internal/logger" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/types" @@ -63,6 +64,8 @@ type defaultAuthProviderConf struct { const DefaultAuthProviderName string = "dockerConfig" const DefaultDockerAuthTTL = 1 * time.Hour +var logOpt = logger.Option{ComponentType: logger.AuthProvider} + // init calls Register for our default provider, which simply reads the .dockercfg file. func init() { Register(DefaultAuthProviderName, &defaultProviderFactory{}) @@ -96,7 +99,7 @@ func (d *defaultAuthProvider) Enabled(_ context.Context) bool { } // Provide reads docker config file and returns corresponding credentials from file if exists -func (d *defaultAuthProvider) Provide(_ context.Context, artifact string) (AuthConfig, error) { +func (d *defaultAuthProvider) Provide(ctx context.Context, artifact string) (AuthConfig, error) { // load docker config file at default path if config file path not specified var cfg *configfile.ConfigFile if d.configPath == "" { @@ -124,7 +127,16 @@ func (d *defaultAuthProvider) Provide(_ context.Context, artifact string) (AuthC return AuthConfig{}, re.ErrorCodeHostNameInvalid.WithError(err).WithComponentType(re.AuthProvider) } - dockerAuthConfig := cfg.AuthConfigs[artifactHostName] + dockerAuthConfig, exists := cfg.AuthConfigs[artifactHostName] + if !exists { + logger.GetLogger(ctx, logOpt).Debugf("no credentials found for registry hostname: %s", artifactHostName) + hostnames := []string{} + for k := range cfg.AuthConfigs { + hostnames = append(hostnames, k) + } + logger.GetLogger(ctx, logOpt).Debugf("list of registry host names in config : %v", hostnames) + return AuthConfig{}, nil + } if dockerAuthConfig == (types.AuthConfig{}) { return AuthConfig{}, nil } diff --git a/pkg/common/oras/authprovider/k8secret_authprovider.go b/pkg/common/oras/authprovider/k8secret_authprovider.go index 2c4a71ccc..10fd74631 100644 --- a/pkg/common/oras/authprovider/k8secret_authprovider.go +++ b/pkg/common/oras/authprovider/k8secret_authprovider.go @@ -25,10 +25,12 @@ import ( "time" re "github.com/deislabs/ratify/errors" + "github.com/deislabs/ratify/internal/logger" "github.com/deislabs/ratify/pkg/utils" "github.com/docker/cli/cli/config" core "k8s.io/api/core/v1" + e "k8s.io/apimachinery/pkg/api/errors" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -38,7 +40,7 @@ type k8SecretProviderFactory struct{} type k8SecretAuthProvider struct { ratifyNamespace string config k8SecretAuthProviderConf - clusterClientSet *kubernetes.Clientset + clusterClientSet kubernetes.Interface } type secretConfig struct { @@ -121,6 +123,7 @@ func (d *k8SecretAuthProvider) Provide(ctx context.Context, artifact string) (Au return AuthConfig{}, re.ErrorCodeHostNameInvalid.WithError(err).WithComponentType(re.AuthProvider) } + logger.GetLogger(ctx, logOpt).Debugf("attempting to resolve credentials for registry hostname: %s", hostName) // iterate through config secrets, resolve each secret, and store in map for _, k8secret := range d.config.Secrets { // default value of secret is assumed to be ratify namespace @@ -129,13 +132,16 @@ func (d *k8SecretAuthProvider) Provide(ctx context.Context, artifact string) (Au } secret, err := d.clusterClientSet.CoreV1().Secrets(k8secret.Namespace).Get(ctx, k8secret.SecretName, meta.GetOptions{}) - if err != nil { + if e.IsNotFound(err) { + logger.GetLogger(ctx, logOpt).Debugf("secret %s not found in namespace %s", k8secret.SecretName, k8secret.Namespace) + continue + } else if err != nil { return AuthConfig{}, re.ErrorCodeGetClusterResourceFailure.NewError(re.AuthProvider, "", re.EmptyLink, err, fmt.Sprintf("failed to pull secret %s from cluster.", k8secret.SecretName), re.HideStackTrace) } // only docker config json secret type allowed if secret.Type == core.SecretTypeDockerConfigJson { - authConfig, err := d.resolveCredentialFromSecret(hostName, secret) + authConfig, err := d.resolveCredentialFromSecret(ctx, hostName, secret) if err != nil && !errors.Is(err, re.ErrorCodeNoMatchingCredential) { return AuthConfig{}, err } @@ -157,13 +163,16 @@ func (d *k8SecretAuthProvider) Provide(ctx context.Context, artifact string) (Au // extract the imagePullSecrets linked to service account for _, imagePullSecret := range serviceAccount.ImagePullSecrets { secret, err := d.clusterClientSet.CoreV1().Secrets(d.ratifyNamespace).Get(ctx, imagePullSecret.Name, meta.GetOptions{}) - if err != nil { + if e.IsNotFound(err) { + logger.GetLogger(ctx, logOpt).Debugf("image pull secret %s not found in namespace %s", imagePullSecret.Name, d.ratifyNamespace) + continue + } else if err != nil { return AuthConfig{}, re.ErrorCodeGetClusterResourceFailure.WithError(err).WithComponentType(re.AuthProvider) } // only dockercfg or docker config json secret type allowed if secret.Type == core.SecretTypeDockercfg || secret.Type == core.SecretTypeDockerConfigJson { - authConfig, err := d.resolveCredentialFromSecret(hostName, secret) + authConfig, err := d.resolveCredentialFromSecret(ctx, hostName, secret) if err != nil && !errors.Is(err, re.ErrorCodeNoMatchingCredential) { return AuthConfig{}, err } @@ -171,13 +180,15 @@ func (d *k8SecretAuthProvider) Provide(ctx context.Context, artifact string) (Au if err == nil { return authConfig, nil } + } else { + logger.GetLogger(ctx, logOpt).Debugf("image pull secret %s of type %s not supported", imagePullSecret.Name, secret.Type) } } return AuthConfig{}, fmt.Errorf("could not find credentials for %s", artifact) } -func (d *k8SecretAuthProvider) resolveCredentialFromSecret(hostName string, secret *core.Secret) (AuthConfig, error) { +func (d *k8SecretAuthProvider) resolveCredentialFromSecret(ctx context.Context, hostName string, secret *core.Secret) (AuthConfig, error) { dockercfg, exists := secret.Data[core.DockerConfigJsonKey] if !exists { return AuthConfig{}, re.ErrorCodeConfigInvalid.WithDetail("could not extract auth configs from docker config") @@ -190,6 +201,12 @@ func (d *k8SecretAuthProvider) resolveCredentialFromSecret(hostName string, secr authConfig, exist := configFile.AuthConfigs[hostName] if !exist { + logger.GetLogger(ctx, logOpt).Debugf("host name %s not found in image pull secret %s", hostName, secret.Name) + hostnames := []string{} + for k := range configFile.AuthConfigs { + hostnames = append(hostnames, k) + } + logger.GetLogger(ctx, logOpt).Debugf("list of host names in image pull secret %s: %v", secret.Name, hostnames) return AuthConfig{}, re.ErrorCodeNoMatchingCredential } diff --git a/pkg/common/oras/authprovider/k8secret_authprovider_test.go b/pkg/common/oras/authprovider/k8secret_authprovider_test.go index c925b3147..52be7957f 100644 --- a/pkg/common/oras/authprovider/k8secret_authprovider_test.go +++ b/pkg/common/oras/authprovider/k8secret_authprovider_test.go @@ -16,16 +16,19 @@ limitations under the License. package authprovider import ( + "context" "errors" "testing" ratifyerrors "github.com/deislabs/ratify/errors" core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" ) -// Checks K8s Docker Json Config Secret is properly extracted and -// credentials returned when Provide is called -func TestProvide_K8SecretDockerConfigJson_ReturnsExpected(t *testing.T) { +// TestResolveCredentialFromSecret_K8SecretDockerConfigJson_ReturnsExpected checks +// K8s Docker Json Config Secret is properly extracted and credentials returned when Provide is called +func TestResolveCredentialFromSecret_K8SecretDockerConfigJson_ReturnsExpected(t *testing.T) { var testSecret core.Secret testSecret.Data = make(map[string][]byte) testSecret.Data[core.DockerConfigJsonKey] = []byte(secretContent) @@ -33,7 +36,7 @@ func TestProvide_K8SecretDockerConfigJson_ReturnsExpected(t *testing.T) { var k8secretprovider k8SecretAuthProvider - authConfig, err := k8secretprovider.resolveCredentialFromSecret("index.docker.io", &testSecret) + authConfig, err := k8secretprovider.resolveCredentialFromSecret(context.Background(), "index.docker.io", &testSecret) if err != nil { t.Fatalf("resolveCredentialFromSecret failed to get credential with err %v", err) } @@ -43,7 +46,9 @@ func TestProvide_K8SecretDockerConfigJson_ReturnsExpected(t *testing.T) { } } -func TestProvide_K8SecretDockerConfigJsonWithIdentityToken_ReturnsExpected(t *testing.T) { +// TestResolveCredentialFromSecret_K8SecretDockerConfigJsonWithIdentityToken_ReturnsExpected checks +// K8s Docker Json Config Secret is properly extracted and credentials returned when Provide is called with an identity token +func TestResolveCredentialFromSecret_K8SecretDockerConfigJsonWithIdentityToken_ReturnsExpected(t *testing.T) { var testSecret core.Secret testSecret.Data = make(map[string][]byte) testSecret.Data[core.DockerConfigJsonKey] = []byte(secretContentIdentityToken) @@ -51,7 +56,7 @@ func TestProvide_K8SecretDockerConfigJsonWithIdentityToken_ReturnsExpected(t *te var k8secretprovider k8SecretAuthProvider - authConfig, err := k8secretprovider.resolveCredentialFromSecret("index.docker.io", &testSecret) + authConfig, err := k8secretprovider.resolveCredentialFromSecret(context.Background(), "index.docker.io", &testSecret) if err != nil { t.Fatalf("resolveCredentialFromSecret failed to get credential with err %v", err) } @@ -62,7 +67,7 @@ func TestProvide_K8SecretDockerConfigJsonWithIdentityToken_ReturnsExpected(t *te } // Checks an error is returned for non-existent registry credential -func TestProvide_K8SecretNonExistentRegistry_ReturnsExpected(t *testing.T) { +func TestResolveCredentialFromSecret_K8SecretNonExistentRegistry_ReturnsExpected(t *testing.T) { var testSecret core.Secret testSecret.Data = make(map[string][]byte) testSecret.Data[core.DockerConfigJsonKey] = []byte(secretContent) @@ -70,7 +75,288 @@ func TestProvide_K8SecretNonExistentRegistry_ReturnsExpected(t *testing.T) { var k8secretprovider k8SecretAuthProvider - if _, err := k8secretprovider.resolveCredentialFromSecret("nonexistent.ghcr.io", &testSecret); !errors.Is(err, ratifyerrors.ErrorCodeNoMatchingCredential) { + if _, err := k8secretprovider.resolveCredentialFromSecret(context.Background(), "nonexistent.ghcr.io", &testSecret); !errors.Is(err, ratifyerrors.ErrorCodeNoMatchingCredential) { t.Fatalf("resolveCredentialFromSecret should have failed to get credential with err %v but returned err %v", ratifyerrors.ErrorCodeNoMatchingCredential, err) } } + +// TestProvide_NotEnabled_ReturnsExpected tests that the Provide method +// returns an error when the k8SecretAuthProvider is not enabled +func TestProvide_NotEnabled_ReturnsExpected(t *testing.T) { + var k8secretprovider k8SecretAuthProvider + + if _, err := k8secretprovider.Provide(context.Background(), "nonexistent.ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_InvalidHostName_ReturnsExpected tests that the Provide method +// returns an error when the hostname is invalid +func TestProvide_InvalidHostName_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(), + } + + if _, err := k8secretprovider.Provide(context.Background(), "badhostname/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_SecretNotFound_ReturnsExpected tests that the Provide method +// returns an error when the secret is not found +func TestProvider_SecretNotFound_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-matching-secret", + Namespace: "gatekeeper-system", + }, + }), + config: k8SecretAuthProviderConf{ + Secrets: []secretConfig{ + { + SecretName: "test-secret", + }, + }, + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_SecretIncorrectType_ReturnsExpected tests that the Provide method +// returns an error when the secret type is incorrect +func TestProvider_IncorrectSecretType_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "gatekeeper-system", + }, + Type: core.SecretTypeBasicAuth, + }), + config: k8SecretAuthProviderConf{ + Secrets: []secretConfig{ + { + SecretName: "test-secret", + }, + }, + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_NoMatchingCredential_ReturnsExpected tests that the Provide method +// returns an error when no matching credential is found +func TestProvider_NoMatchingCredential_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "gatekeeper-system", + }, + Type: core.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + core.DockerConfigJsonKey: []byte(secretContent), + }, + }), + config: k8SecretAuthProviderConf{ + Secrets: []secretConfig{ + { + SecretName: "test-secret", + }, + }, + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_ServiceAccountNotFound_ReturnsExpected tests that the Provide method +// returns an error when the service account has no image pull secrets +func TestProvider_ServiceAccountNoSecrets_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ratify-admin", + Namespace: "gatekeeper-system", + }, + }), + config: k8SecretAuthProviderConf{ + ServiceAccountName: "ratify-admin", + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_ServiceAccountSecretNotFound_ReturnsExpected tests that the Provide method +// returns an error when the service account refers to a secret that does not exist +func TestProvider_ServiceAccountSecretNotFound_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ratify-admin", + Namespace: "gatekeeper-system", + }, + ImagePullSecrets: []core.LocalObjectReference{ + { + Name: "non-existent-secret", + }, + }, + }), + config: k8SecretAuthProviderConf{ + ServiceAccountName: "ratify-admin", + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_ServiceAccountSecretIncorrectType_ReturnsExpected tests that the Provide method +// returns an error when the service account refers to a secret with an incorrect type +func TestProvider_ServiceAccountSecretIncorrectType_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ratify-admin", + Namespace: "gatekeeper-system", + }, + ImagePullSecrets: []core.LocalObjectReference{ + { + Name: "test-secret", + }, + }, + }, &core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "gatekeeper-system", + }, + Type: core.SecretTypeBasicAuth, + }), + config: k8SecretAuthProviderConf{ + ServiceAccountName: "ratify-admin", + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_ServiceAccountNoMatchingCredential_ReturnsExpected tests that the Provide method +// returns an error when no matching credential is found for any of the service account image pull secrets +func TestProvider_ServiceAccountNoMatchingCredential_ReturnsExpected(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ratify-admin", + Namespace: "gatekeeper-system", + }, + ImagePullSecrets: []core.LocalObjectReference{ + { + Name: "test-secret", + }, + }, + }, &core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "gatekeeper-system", + }, + Type: core.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + core.DockerConfigJsonKey: []byte(secretContent), + }, + }), + config: k8SecretAuthProviderConf{ + ServiceAccountName: "ratify-admin", + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "ghcr.io/artifact:v1"); err == nil { + t.Fatalf("Provide should have failed to get credential with err but returned nil") + } +} + +// TestProvide_ServiceAccountSecretFound_ReturnsSuccess tests that the Provide method +// returns auth config when a matching credential is found for a user defined secret +func TestProvider_SecretFound_ReturnsSuccess(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "gatekeeper-system", + }, + Type: core.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + core.DockerConfigJsonKey: []byte(secretContent), + }, + }), + config: k8SecretAuthProviderConf{ + Secrets: []secretConfig{ + { + SecretName: "test-secret", + }, + }, + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "index.docker.io/artifact:v1"); err != nil { + t.Fatalf("Provide failed to get credential with err %v", err) + } +} + +// TestProvide_ServiceAccountSecretFound_ReturnsSuccess tests that the Provide method +// returns auth config when a matching credential is found for a service account image pull secret +func TestProvider_ServiceAccountSecretFound_ReturnsSuccess(t *testing.T) { + k8secretprovider := k8SecretAuthProvider{ + ratifyNamespace: "gatekeeper-system", + clusterClientSet: fake.NewSimpleClientset(&core.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ratify-admin", + Namespace: "gatekeeper-system", + }, + ImagePullSecrets: []core.LocalObjectReference{ + { + Name: "test-secret", + }, + }, + }, &core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "gatekeeper-system", + }, + Type: core.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + core.DockerConfigJsonKey: []byte(secretContent), + }, + }), + config: k8SecretAuthProviderConf{ + ServiceAccountName: "ratify-admin", + }, + } + + if _, err := k8secretprovider.Provide(context.Background(), "index.docker.io/artifact:v1"); err != nil { + t.Fatalf("Provide failed to get credential with err %v", err) + } +}