From f4959349a8d029a06b6c87d887366b6b8996df51 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Mon, 19 Aug 2024 19:27:39 -0500 Subject: [PATCH] feat: KMP periodic retrieval with k8s requeue (#1727) --- api/v1beta1/keymanagementproviders_types.go | 4 + .../namespacedkeymanagementprovider_types.go | 4 + ...mentprovider-customresourcedefinition.yaml | 152 +++---- ...mentprovider-customresourcedefinition.yaml | 155 +++---- ...fy.deislabs.io_keymanagementproviders.yaml | 8 + ...s.io_namespacedkeymanagementproviders.yaml | 6 + ...fig_v1beta1_keymanagementprovider_akv.yaml | 6 +- ...anagementprovider_akv_refresh_enabled.yaml | 14 + ...fig_v1beta1_keymanagementprovider_akv.yaml | 6 +- ...anagementprovider_akv_refresh_enabled.yaml | 14 + .../keymanagementprovider_controller.go | 131 +----- .../keymanagementprovider_controller_test.go | 338 +++++----------- .../keymanagementprovider_controller.go | 131 +----- .../keymanagementprovider_controller_test.go | 336 +++++----------- .../azurekeyvault/provider.go | 4 + .../azurekeyvault/provider_test.go | 20 + pkg/keymanagementprovider/inline/provider.go | 4 + .../inline/provider_test.go | 37 ++ .../keymanagementprovider.go | 2 + pkg/keymanagementprovider/mocks/client.go | 32 ++ pkg/keymanagementprovider/mocks/factory.go | 33 ++ pkg/keymanagementprovider/mocks/types.go | 4 + pkg/keymanagementprovider/refresh/factory.go | 53 +++ .../refresh/factory_test.go | 119 ++++++ .../refresh/kubeRefresh.go | 210 ++++++++++ .../refresh/kubeRefreshNamedspaced_test.go | 376 +++++++++++++++++ .../refresh/kubeRefreshNamespaced.go | 208 ++++++++++ .../refresh/kubeRefresh_test.go | 380 ++++++++++++++++++ pkg/keymanagementprovider/refresh/refresh.go | 34 ++ .../refresh/test_helper_test.go | 26 ++ 30 files changed, 2019 insertions(+), 828 deletions(-) create mode 100644 config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml create mode 100644 config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml create mode 100644 pkg/keymanagementprovider/mocks/client.go create mode 100644 pkg/keymanagementprovider/mocks/factory.go create mode 100644 pkg/keymanagementprovider/refresh/factory.go create mode 100644 pkg/keymanagementprovider/refresh/factory_test.go create mode 100644 pkg/keymanagementprovider/refresh/kubeRefresh.go create mode 100644 pkg/keymanagementprovider/refresh/kubeRefreshNamedspaced_test.go create mode 100644 pkg/keymanagementprovider/refresh/kubeRefreshNamespaced.go create mode 100644 pkg/keymanagementprovider/refresh/kubeRefresh_test.go create mode 100644 pkg/keymanagementprovider/refresh/refresh.go create mode 100644 pkg/keymanagementprovider/refresh/test_helper_test.go diff --git a/api/v1beta1/keymanagementproviders_types.go b/api/v1beta1/keymanagementproviders_types.go index d957f382e..2797ccac2 100644 --- a/api/v1beta1/keymanagementproviders_types.go +++ b/api/v1beta1/keymanagementproviders_types.go @@ -31,6 +31,10 @@ type KeyManagementProviderSpec struct { // Name of the key management provider Type string `json:"type,omitempty"` + // Refresh interval for fetching the certificate/key files from the provider. Only for providers that are refreshable. The value is in the format of "1h30m" where "h" means hour and "m" means minute. Valid time units are units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + // +kubebuilder:default="" + RefreshInterval string `json:"refreshInterval,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields // Parameters of the key management provider Parameters runtime.RawExtension `json:"parameters,omitempty"` diff --git a/api/v1beta1/namespacedkeymanagementprovider_types.go b/api/v1beta1/namespacedkeymanagementprovider_types.go index f4d3e5839..d124e88f1 100644 --- a/api/v1beta1/namespacedkeymanagementprovider_types.go +++ b/api/v1beta1/namespacedkeymanagementprovider_types.go @@ -32,6 +32,10 @@ type NamespacedKeyManagementProviderSpec struct { // Name of the key management provider Type string `json:"type,omitempty"` + // Refresh interval for the key management provider. Only used if the key management provider is refreshable. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + // +kubebuilder:default="" + RefreshInterval string `json:"refreshInterval,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields // Parameters of the key management provider Parameters runtime.RawExtension `json:"parameters,omitempty"` diff --git a/charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml b/charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml index ec41a63fe..bbb7a9c13 100644 --- a/charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml +++ b/charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml @@ -14,73 +14,87 @@ spec: singular: keymanagementprovider scope: Cluster versions: - - additionalPrinterColumns: - - jsonPath: .status.issuccess - name: IsSuccess - type: boolean - - jsonPath: .status.brieferror - name: Error - type: string - - jsonPath: .status.lastfetchedtime - name: LastFetchedTime - type: date - name: v1beta1 - schema: - openAPIV3Schema: - description: KeyManagementProvider is the Schema for the keymanagementproviders - API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: KeyManagementProviderSpec defines the desired state of KeyManagementProvider - properties: - parameters: - description: Parameters of the key management provider - type: object - x-kubernetes-preserve-unknown-fields: true - type: - description: Name of the key management provider - type: string - type: object - status: - description: KeyManagementProviderStatus defines the observed state of - KeyManagementProvider - properties: - brieferror: - description: Truncated error message if the message is too long - type: string - error: - description: Error message if operation was unsuccessful - type: string - issuccess: - description: Is successful in loading certificate/key files - type: boolean - lastfetchedtime: - description: The time stamp of last successful certificate/key fetch - operation. If operation failed, last fetched time shows the time - of error - format: date-time - type: string + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + - jsonPath: .status.lastfetchedtime + name: LastFetchedTime + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: + KeyManagementProvider is the Schema for the keymanagementproviders + API + properties: + apiVersion: + description: + "APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources" + type: string + kind: + description: + "Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + metadata: + type: object + spec: + description: KeyManagementProviderSpec defines the desired state of KeyManagementProvider properties: - description: provider specific properties of the each individual certificate/key - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - issuccess - type: object - type: object - served: true - storage: true - subresources: - status: {} + refreshInterval: + default: "" + description: + Refresh interval for fetching the certificate/key files + from the provider. Only for providers that are refreshable. The + value is in the format of "1h30m" where "h" means hour and "m" means + minute. Valid time units are units are "ns", "us" (or "µs"), "ms", + "s", "m", "h". + type: string + parameters: + description: Parameters of the key management provider + type: object + x-kubernetes-preserve-unknown-fields: true + type: + description: Name of the key management provider + type: string + type: object + status: + description: + KeyManagementProviderStatus defines the observed state of + KeyManagementProvider + properties: + brieferror: + description: Truncated error message if the message is too long + type: string + error: + description: Error message if operation was unsuccessful + type: string + issuccess: + description: Is successful in loading certificate/key files + type: boolean + lastfetchedtime: + description: + The time stamp of last successful certificate/key fetch + operation. If operation failed, last fetched time shows the time + of error + format: date-time + type: string + properties: + description: provider specific properties of the each individual certificate/key + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - issuccess + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/ratify/crds/namespacedkeymanagementprovider-customresourcedefinition.yaml b/charts/ratify/crds/namespacedkeymanagementprovider-customresourcedefinition.yaml index ef70f27d9..405517810 100644 --- a/charts/ratify/crds/namespacedkeymanagementprovider-customresourcedefinition.yaml +++ b/charts/ratify/crds/namespacedkeymanagementprovider-customresourcedefinition.yaml @@ -14,74 +14,89 @@ spec: singular: namespacedkeymanagementprovider scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .status.issuccess - name: IsSuccess - type: boolean - - jsonPath: .status.brieferror - name: Error - type: string - - jsonPath: .status.lastfetchedtime - name: LastFetchedTime - type: date - name: v1beta1 - schema: - openAPIV3Schema: - description: NamespacedKeyManagementProvider is the Schema for the namespacedkeymanagementproviders - API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: NamespacedKeyManagementProviderSpec defines the desired state - of NamespacedKeyManagementProvider - properties: - parameters: - description: Parameters of the key management provider - type: object - x-kubernetes-preserve-unknown-fields: true - type: - description: Name of the key management provider - type: string - type: object - status: - description: NamespacedKeyManagementProviderStatus defines the observed - state of NamespacedKeyManagementProvider - properties: - brieferror: - description: Truncated error message if the message is too long - type: string - error: - description: Error message if operation was unsuccessful - type: string - issuccess: - description: Is successful in loading certificate/key files - type: boolean - lastfetchedtime: - description: The time stamp of last successful certificate/key fetch - operation. If operation failed, last fetched time shows the time - of error - format: date-time - type: string + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + - jsonPath: .status.lastfetchedtime + name: LastFetchedTime + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: + NamespacedKeyManagementProvider is the Schema for the namespacedkeymanagementproviders + API + properties: + apiVersion: + description: + "APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources" + type: string + kind: + description: + "Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + metadata: + type: object + spec: + description: + NamespacedKeyManagementProviderSpec defines the desired state + of NamespacedKeyManagementProvider properties: - description: provider specific properties of the each individual certificate/key - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - issuccess - type: object - type: object - served: true - storage: true - subresources: - status: {} + refreshInterval: + default: "" + description: + Refresh interval for fetching the certificate/key files + from the provider. Only for providers that are refreshable. The + value is in the format of "1h30m" where "h" means hour and "m" means + minute. Valid time units are units are "ns", "us" (or "µs"), "ms", + "s", "m", "h". + type: string + parameters: + description: Parameters of the key management provider + type: object + x-kubernetes-preserve-unknown-fields: true + type: + description: Name of the key management provider + type: string + type: object + status: + description: + NamespacedKeyManagementProviderStatus defines the observed + state of NamespacedKeyManagementProvider + properties: + brieferror: + description: Truncated error message if the message is too long + type: string + error: + description: Error message if operation was unsuccessful + type: string + issuccess: + description: Is successful in loading certificate/key files + type: boolean + lastfetchedtime: + description: + The time stamp of last successful certificate/key fetch + operation. If operation failed, last fetched time shows the time + of error + format: date-time + type: string + properties: + description: provider specific properties of the each individual certificate/key + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - issuccess + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml b/config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml index d924d7dc8..2a3658da0 100644 --- a/config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml +++ b/config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml @@ -54,6 +54,14 @@ spec: description: Parameters of the key management provider type: object x-kubernetes-preserve-unknown-fields: true + refreshInterval: + default: "" + description: Refresh interval for fetching the certificate/key files + from the provider. Only for providers that are refreshable. The + value is in the format of "1h30m" where "h" means hour and "m" means + minute. Valid time units are units are "ns", "us" (or "µs"), "ms", + "s", "m", "h". + type: string type: description: Name of the key management provider type: string diff --git a/config/crd/bases/config.ratify.deislabs.io_namespacedkeymanagementproviders.yaml b/config/crd/bases/config.ratify.deislabs.io_namespacedkeymanagementproviders.yaml index 188294d4c..0054b1e5b 100644 --- a/config/crd/bases/config.ratify.deislabs.io_namespacedkeymanagementproviders.yaml +++ b/config/crd/bases/config.ratify.deislabs.io_namespacedkeymanagementproviders.yaml @@ -55,6 +55,12 @@ spec: description: Parameters of the key management provider type: object x-kubernetes-preserve-unknown-fields: true + refreshInterval: + default: "" + description: Refresh interval for the key management provider. Only + used if the key management provider is refreshable. Valid time units + are "ns", "us" (or "µs"), "ms", "s", "m", "h". + type: string type: description: Name of the key management provider type: string diff --git a/config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv.yaml b/config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv.yaml index ae25f7f6a..7f9229014 100644 --- a/config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv.yaml +++ b/config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv.yaml @@ -1,13 +1,13 @@ apiVersion: config.ratify.deislabs.io/v1beta1 kind: KeyManagementProvider metadata: - name: keymanagementprovider-inline + name: keymanagementprovider-akv spec: type: azurekeyvault parameters: vaultURI: https://yourkeyvault.vault.azure.net/ certificates: - name: yourCertName - version: yourCertVersion # Optional, fetch latest version if empty + version: yourCertVersion # Optional, fetch latest version if empty tenantID: - clientID: + clientID: diff --git a/config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml b/config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml new file mode 100644 index 000000000..3b820f616 --- /dev/null +++ b/config/samples/clustered/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml @@ -0,0 +1,14 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: KeyManagementProvider +metadata: + name: keymanagementprovider-akv +spec: + type: azurekeyvault + refreshInterval: 1m + parameters: + vaultURI: https://yourkeyvault.vault.azure.net/ + certificates: + - name: yourCertName + version: yourCertVersion # Optional, fetch latest version if empty + tenantID: + clientID: diff --git a/config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv.yaml b/config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv.yaml index e8971e1bc..7b9affff2 100644 --- a/config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv.yaml +++ b/config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv.yaml @@ -1,13 +1,13 @@ apiVersion: config.ratify.deislabs.io/v1beta1 kind: NamespacedKeyManagementProvider metadata: - name: keymanagementprovider-inline + name: keymanagementprovider-akv spec: type: azurekeyvault parameters: vaultURI: https://yourkeyvault.vault.azure.net/ certificates: - name: yourCertName - version: yourCertVersion # Optional, fetch latest version if empty + version: yourCertVersion # Optional, fetch latest version if empty tenantID: - clientID: + clientID: diff --git a/config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml b/config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml new file mode 100644 index 000000000..28aeaacb2 --- /dev/null +++ b/config/samples/namespaced/kmp/config_v1beta1_keymanagementprovider_akv_refresh_enabled.yaml @@ -0,0 +1,14 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: NamespacedKeyManagementProvider +metadata: + name: keymanagementprovider-akv +spec: + type: azurekeyvault + refreshInterval: 1m + parameters: + vaultURI: https://yourkeyvault.vault.azure.net/ + certificates: + - name: yourCertName + version: yourCertVersion # Optional, fetch latest version if empty + tenantID: + clientID: diff --git a/pkg/controllers/clusterresource/keymanagementprovider_controller.go b/pkg/controllers/clusterresource/keymanagementprovider_controller.go index 11a32ed81..93dda9806 100644 --- a/pkg/controllers/clusterresource/keymanagementprovider_controller.go +++ b/pkg/controllers/clusterresource/keymanagementprovider_controller.go @@ -18,24 +18,17 @@ package clusterresource import ( "context" - "encoding/json" "fmt" - "maps" - "github.com/ratify-project/ratify/internal/constants" _ "github.com/ratify-project/ratify/pkg/keymanagementprovider/azurekeyvault" // register azure key vault key management provider _ "github.com/ratify-project/ratify/pkg/keymanagementprovider/inline" // register inline key management provider - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/ratify-project/ratify/pkg/keymanagementprovider/refresh" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" - cutils "github.com/ratify-project/ratify/pkg/controllers/utils" - kmp "github.com/ratify-project/ratify/pkg/keymanagementprovider" - "github.com/sirupsen/logrus" ) // KeyManagementProviderReconciler reconciles a KeyManagementProvider object @@ -44,76 +37,33 @@ type KeyManagementProviderReconciler struct { Scheme *runtime.Scheme } -// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders/finalizers,verbs=update -func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := logrus.WithContext(ctx) - - var resource = req.Name - var keyManagementProvider configv1beta1.KeyManagementProvider - - logger.Infof("reconciling cluster key management provider '%v'", resource) - - if err := r.Get(ctx, req.NamespacedName, &keyManagementProvider); err != nil { - if apierrors.IsNotFound(err) { - logger.Infof("deletion detected, removing key management provider %v", resource) - kmp.DeleteCertificatesFromMap(resource) - kmp.DeleteKeysFromMap(resource) - } else { - logger.Error(err, "unable to fetch key management provider") - } - - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - lastFetchedTime := metav1.Now() - isFetchSuccessful := false - - // get certificate store list to check if certificate store is configured - // TODO: remove check in v2.0.0+ - var certificateStoreList configv1beta1.CertificateStoreList - if err := r.List(ctx, &certificateStoreList); err != nil { - logger.Error(err, "unable to list certificate stores") +func (r *KeyManagementProviderReconciler) ReconcileWithConfig(ctx context.Context, config map[string]interface{}) (ctrl.Result, error) { + refresher, err := refresh.CreateRefresherFromConfig(config) + if err != nil { return ctrl.Result{}, err } - // if certificate store is configured, return error. Only one of certificate store and key management provider can be configured - if len(certificateStoreList.Items) > 0 { - // Note: for backwards compatibility in upgrade scenarios, Ratify will only log a warning statement. - logger.Warn("Certificate Store already exists. Key management provider and certificate store should not be configured together. Please migrate to key management provider and delete certificate store.") - } - - provider, err := cutils.SpecToKeyManagementProvider(keyManagementProvider.Spec.Parameters.Raw, keyManagementProvider.Spec.Type) + err = refresher.Refresh(ctx) if err != nil { - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) return ctrl.Result{}, err } - // fetch certificates and store in map - certificates, certAttributes, err := provider.GetCertificates(ctx) - if err != nil { - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) - return ctrl.Result{}, fmt.Errorf("Error fetching certificates in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) + result, ok := refresher.GetResult().(ctrl.Result) + if !ok { + return ctrl.Result{}, fmt.Errorf("unexpected type returned from GetResult: %T", refresher.GetResult()) } + return result, nil +} - // fetch keys and store in map - keys, keyAttributes, err := provider.GetKeys(ctx) - if err != nil { - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) - return ctrl.Result{}, fmt.Errorf("Error fetching keys in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders/finalizers,verbs=update +func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + refresherConfig := map[string]interface{}{ + "type": refresh.KubeRefresherType, + "client": r.Client, + "request": req, } - kmp.SetCertificatesInMap(resource, certificates) - kmp.SetKeysInMap(resource, keyManagementProvider.Spec.Type, keys) - // merge certificates and keys status into one - maps.Copy(keyAttributes, certAttributes) - isFetchSuccessful = true - emptyErrorString := "" - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, emptyErrorString, lastFetchedTime, keyAttributes) - - logger.Infof("%v certificate(s) & %v key(s) fetched for key management provider %v", len(certificates), len(keys), resource) - - // returning empty result and no error to indicate we’ve successfully reconciled this object - return ctrl.Result{}, nil + return r.ReconcileWithConfig(ctx, refresherConfig) } // SetupWithManager sets up the controller with the Manager. @@ -127,46 +77,3 @@ func (r *KeyManagementProviderReconciler) SetupWithManager(mgr ctrl.Manager) err For(&configv1beta1.KeyManagementProvider{}).WithEventFilter(pred). Complete(r) } - -// writeKMProviderStatus updates the status of the key management provider resource -func writeKMProviderStatus(ctx context.Context, r client.StatusClient, keyManagementProvider *configv1beta1.KeyManagementProvider, logger *logrus.Entry, isSuccess bool, errorString string, operationTime metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { - if isSuccess { - updateKMProviderSuccessStatus(keyManagementProvider, &operationTime, kmProviderStatus) - } else { - updateKMProviderErrorStatus(keyManagementProvider, errorString, &operationTime) - } - if statusErr := r.Status().Update(ctx, keyManagementProvider); statusErr != nil { - logger.Error(statusErr, ",unable to update key management provider error status") - } -} - -// updateKMProviderErrorStatus updates the key management provider status with error, brief error and last fetched time -func updateKMProviderErrorStatus(keyManagementProvider *configv1beta1.KeyManagementProvider, errorString string, operationTime *metav1.Time) { - // truncate brief error string to maxBriefErrLength - briefErr := errorString - if len(errorString) > constants.MaxBriefErrLength { - briefErr = fmt.Sprintf("%s...", errorString[:constants.MaxBriefErrLength]) - } - keyManagementProvider.Status.IsSuccess = false - keyManagementProvider.Status.Error = errorString - keyManagementProvider.Status.BriefError = briefErr - keyManagementProvider.Status.LastFetchedTime = operationTime -} - -// updateKMProviderSuccessStatus updates the key management provider status if status argument is non nil -// Success status includes last fetched time and other provider-specific properties -func updateKMProviderSuccessStatus(keyManagementProvider *configv1beta1.KeyManagementProvider, lastOperationTime *metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { - keyManagementProvider.Status.IsSuccess = true - keyManagementProvider.Status.Error = "" - keyManagementProvider.Status.BriefError = "" - keyManagementProvider.Status.LastFetchedTime = lastOperationTime - - if kmProviderStatus != nil { - jsonString, _ := json.Marshal(kmProviderStatus) - - raw := runtime.RawExtension{ - Raw: jsonString, - } - keyManagementProvider.Status.Properties = raw - } -} diff --git a/pkg/controllers/clusterresource/keymanagementprovider_controller_test.go b/pkg/controllers/clusterresource/keymanagementprovider_controller_test.go index caa20114b..d4129e2c6 100644 --- a/pkg/controllers/clusterresource/keymanagementprovider_controller_test.go +++ b/pkg/controllers/clusterresource/keymanagementprovider_controller_test.go @@ -17,267 +17,143 @@ package clusterresource import ( "context" - "fmt" + "errors" + "reflect" "testing" - configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" - "github.com/ratify-project/ratify/internal/constants" - "github.com/ratify-project/ratify/pkg/keymanagementprovider" - "github.com/sirupsen/logrus" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - + "github.com/ratify-project/ratify/pkg/keymanagementprovider/refresh" test "github.com/ratify-project/ratify/pkg/utils" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" -) - -const ( - kmpName = "kmpName" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -// TestUpdateErrorStatus tests the updateErrorStatus method -func TestKMProviderUpdateErrorStatus(t *testing.T) { - var parametersString = "{\"certs\":{\"name\":\"certName\"}}" - var kmProviderStatus = []byte(parametersString) - - status := configv1beta1.KeyManagementProviderStatus{ - IsSuccess: true, - Properties: runtime.RawExtension{ - Raw: kmProviderStatus, +func TestKeyManagementProviderReconciler_ReconcileWithConfig(t *testing.T) { + tests := []struct { + name string + refresherType string + createConfigError bool + refreshError bool + expectedError bool + }{ + { + name: "Successful Reconcile", + refresherType: "mockRefresher", + createConfigError: false, + refreshError: false, + expectedError: false, + }, + { + name: "Refresher Error", + refresherType: "mockRefresher", + createConfigError: false, + refreshError: true, + expectedError: true, + }, + { + name: "Invalid Refresher Type", + refresherType: "invalidRefresher", + createConfigError: true, + refreshError: false, + expectedError: true, }, } - keyManagementProvider := configv1beta1.KeyManagementProvider{ - Status: status, - } - expectedErr := "it's a long error from unit test" - lastFetchedTime := metav1.Now() - updateKMProviderErrorStatus(&keyManagementProvider, expectedErr, &lastFetchedTime) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: "fake-name", + Namespace: "fake-namespace", + }, + } + scheme, _ := test.CreateScheme() + client := fake.NewClientBuilder().WithScheme(scheme).Build() - if keyManagementProvider.Status.IsSuccess != false { - t.Fatalf("Unexpected error, expected isSuccess to be false , actual %+v", keyManagementProvider.Status.IsSuccess) - } + r := &KeyManagementProviderReconciler{ + Client: client, + Scheme: runtime.NewScheme(), + } - if keyManagementProvider.Status.Error != expectedErr { - t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedErr, keyManagementProvider.Status.Error) - } - expectedBriedErr := fmt.Sprintf("%s...", expectedErr[:30]) - if keyManagementProvider.Status.BriefError != expectedBriedErr { - t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedBriedErr, keyManagementProvider.Status.Error) - } + refresherConfig := map[string]interface{}{ + "type": tt.refresherType, + "client": client, + "request": req, + "createConfigError": tt.createConfigError, + "refreshError": tt.refreshError, + "shouldError": tt.expectedError, + } - //make sure properties of last cached cert was not overridden - if len(keyManagementProvider.Status.Properties.Raw) == 0 { - t.Fatalf("Unexpected properties, expected %+v, got %+v", parametersString, string(keyManagementProvider.Status.Properties.Raw)) + _, err := r.ReconcileWithConfig(context.TODO(), refresherConfig) + if tt.expectedError && err == nil { + t.Errorf("Expected error, got nil") + } + if !tt.expectedError && err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) } } - -// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method -func TestKMProviderUpdateSuccessStatus(t *testing.T) { - kmProviderStatus := keymanagementprovider.KeyManagementProviderStatus{} - properties := map[string]string{} - properties["Name"] = "wabbit" - properties["Version"] = "ABC" - - kmProviderStatus["Certificates"] = properties - - lastFetchedTime := metav1.Now() - - status := configv1beta1.KeyManagementProviderStatus{ - IsSuccess: false, - Error: "error from last operation", - } - keyManagementProvider := configv1beta1.KeyManagementProvider{ - Status: status, +func TestKeyManagementProviderReconciler_Reconcile(t *testing.T) { + req := ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: "fake-name", + Namespace: "fake-namespace", + }, } - updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, kmProviderStatus) + // Create a fake client and scheme + scheme, _ := test.CreateScheme() + client := fake.NewClientBuilder().WithScheme(scheme).Build() - if keyManagementProvider.Status.IsSuccess != true { - t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + r := &KeyManagementProviderReconciler{ + Client: client, + Scheme: runtime.NewScheme(), } - if keyManagementProvider.Status.Error != "" { - t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + // Call the Reconcile method + result, err := r.Reconcile(context.TODO(), req) + if err != nil { + t.Errorf("Unexpected error: %v", err) } - //make sure properties of last cached cert was updated - if len(keyManagementProvider.Status.Properties.Raw) == 0 { - t.Fatalf("Properties should not be empty") + // Check the result + expectedResult := ctrl.Result{} + if !reflect.DeepEqual(result, expectedResult) { + t.Errorf("Expected result %v, got %v", expectedResult, result) } } -// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method with empty properties -func TestKMProviderUpdateSuccessStatus_emptyProperties(t *testing.T) { - lastFetchedTime := metav1.Now() - status := configv1beta1.KeyManagementProviderStatus{ - IsSuccess: false, - Error: "error from last operation", - } - keyManagementProvider := configv1beta1.KeyManagementProvider{ - Status: status, - } - - updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, nil) - - if keyManagementProvider.Status.IsSuccess != true { - t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) - } - - if keyManagementProvider.Status.Error != "" { - t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) - } - - //make sure properties of last cached cert was updated - if len(keyManagementProvider.Status.Properties.Raw) != 0 { - t.Fatalf("Properties should be empty") - } +type MockRefresher struct { + Results ctrl.Result + CreateConfigError bool + RefreshError bool + ShouldError bool } -func TestWriteKMProviderStatus(t *testing.T) { - logger := logrus.WithContext(context.Background()) - lastFetchedTime := metav1.Now() - testCases := []struct { - name string - isSuccess bool - kmProvider *configv1beta1.KeyManagementProvider - errString string - reconciler client.StatusClient - }{ - { - name: "success status", - isSuccess: true, - errString: "", - kmProvider: &configv1beta1.KeyManagementProvider{}, - reconciler: &test.MockStatusClient{}, - }, - { - name: "error status", - isSuccess: false, - kmProvider: &configv1beta1.KeyManagementProvider{}, - errString: "a long error string that exceeds the max length of 30 characters", - reconciler: &test.MockStatusClient{}, - }, - { - name: "status update failed", - isSuccess: true, - kmProvider: &configv1beta1.KeyManagementProvider{}, - reconciler: &test.MockStatusClient{ - UpdateFailed: true, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - writeKMProviderStatus(context.Background(), tc.reconciler, tc.kmProvider, logger, tc.isSuccess, tc.errString, lastFetchedTime, nil) - - if tc.kmProvider.Status.IsSuccess != tc.isSuccess { - t.Fatalf("Expected isSuccess to be %+v , actual %+v", tc.isSuccess, tc.kmProvider.Status.IsSuccess) - } - - if tc.kmProvider.Status.Error != tc.errString { - t.Fatalf("Expected Error to be %+v , actual %+v", tc.errString, tc.kmProvider.Status.Error) - } - }) +func (mr *MockRefresher) Refresh(_ context.Context) error { + if mr.RefreshError { + return errors.New("refresh error") } + return nil } -func TestKMPReconcile(t *testing.T) { - tests := []struct { - name string - description string - provider *configv1beta1.KeyManagementProvider - req *reconcile.Request - expectedErr bool - expectedKMPCount int - }{ - { - name: "nonexistent KMP", - description: "Reconciling a non-existent KMP CR, it should be deleted from maps", - req: &reconcile.Request{ - NamespacedName: types.NamespacedName{Name: "nonexistent"}, - }, - provider: &configv1beta1.KeyManagementProvider{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: constants.EmptyNamespace, - Name: kmpName, - }, - Spec: configv1beta1.KeyManagementProviderSpec{ - Type: "inline", - }, - }, - expectedErr: false, - expectedKMPCount: 0, - }, - { - name: "invalid params", - description: "Received invalid parameters of the KMP Spec, it should fail the reconcile and return an error", - provider: &configv1beta1.KeyManagementProvider{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: constants.EmptyNamespace, - Name: kmpName, - }, - Spec: configv1beta1.KeyManagementProviderSpec{ - Type: "inline", - }, - }, - expectedErr: true, - expectedKMPCount: 0, - }, - { - name: "valid params", - description: "Received a valid KMP manifest, it should be added to the cert map", - provider: &configv1beta1.KeyManagementProvider{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: constants.EmptyNamespace, - Name: kmpName, - }, - Spec: configv1beta1.KeyManagementProviderSpec{ - Type: "inline", - Parameters: runtime.RawExtension{ - Raw: []byte(`{"type": "inline", "contentType": "certificate", "value": "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n"}`), - }, - }, - }, - expectedErr: false, - expectedKMPCount: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resetKMP() - scheme, _ := test.CreateScheme() - client := fake.NewClientBuilder().WithScheme(scheme) - client.WithObjects(tt.provider) - r := &KeyManagementProviderReconciler{ - Scheme: scheme, - Client: client.Build(), - } - var req reconcile.Request - if tt.req != nil { - req = *tt.req - } else { - req = reconcile.Request{ - NamespacedName: test.KeyFor(tt.provider), - } - } +func (mr *MockRefresher) GetResult() interface{} { + return ctrl.Result{} +} - _, err := r.Reconcile(context.Background(), req) - if tt.expectedErr != (err != nil) { - t.Fatalf("Reconcile() expected error %v, actual %v", tt.expectedErr, err) - } - certs, _ := keymanagementprovider.GetCertificatesFromMap(context.Background(), kmpName) - if len(certs) != tt.expectedKMPCount { - t.Fatalf("Cert map expected size %v, actual %v", tt.expectedKMPCount, len(certs)) - } - }) +func (mr *MockRefresher) Create(config map[string]interface{}) (refresh.Refresher, error) { + createConfigError := config["createConfigError"].(bool) + refreshError := config["refreshError"].(bool) + if createConfigError { + return nil, errors.New("create error") } + return &MockRefresher{ + CreateConfigError: createConfigError, + RefreshError: refreshError, + }, nil } -func resetKMP() { - keymanagementprovider.DeleteCertificatesFromMap(storeName) +func init() { + refresh.Register("mockRefresher", &MockRefresher{}) } diff --git a/pkg/controllers/namespaceresource/keymanagementprovider_controller.go b/pkg/controllers/namespaceresource/keymanagementprovider_controller.go index 8216dd95b..ba439a254 100644 --- a/pkg/controllers/namespaceresource/keymanagementprovider_controller.go +++ b/pkg/controllers/namespaceresource/keymanagementprovider_controller.go @@ -18,24 +18,17 @@ package namespaceresource import ( "context" - "encoding/json" "fmt" - "maps" - "github.com/ratify-project/ratify/internal/constants" _ "github.com/ratify-project/ratify/pkg/keymanagementprovider/azurekeyvault" // register azure key vault key management provider _ "github.com/ratify-project/ratify/pkg/keymanagementprovider/inline" // register inline key management provider - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/ratify-project/ratify/pkg/keymanagementprovider/refresh" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" - cutils "github.com/ratify-project/ratify/pkg/controllers/utils" - kmp "github.com/ratify-project/ratify/pkg/keymanagementprovider" - "github.com/sirupsen/logrus" ) // KeyManagementProviderReconciler reconciles a KeyManagementProvider object @@ -44,76 +37,33 @@ type KeyManagementProviderReconciler struct { Scheme *runtime.Scheme } -// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedkeymanagementproviders,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedkeymanagementproviders/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedkeymanagementproviders/finalizers,verbs=update -func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := logrus.WithContext(ctx) - - var resource = req.NamespacedName.String() - var keyManagementProvider configv1beta1.NamespacedKeyManagementProvider - - logger.Infof("reconciling namespaced key management provider '%v'", resource) - - if err := r.Get(ctx, req.NamespacedName, &keyManagementProvider); err != nil { - if apierrors.IsNotFound(err) { - logger.Infof("deletion detected, removing key management provider %v", resource) - kmp.DeleteCertificatesFromMap(resource) - kmp.DeleteKeysFromMap(resource) - } else { - logger.Error(err, "unable to fetch key management provider") - } - - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - lastFetchedTime := metav1.Now() - isFetchSuccessful := false - - // get certificate store list to check if certificate store is configured - // TODO: remove check in v2.0.0+ - var certificateStoreList configv1beta1.CertificateStoreList - if err := r.List(ctx, &certificateStoreList); err != nil { - logger.Error(err, "unable to list certificate stores") +func (r *KeyManagementProviderReconciler) ReconcileWithConfig(ctx context.Context, config map[string]interface{}) (ctrl.Result, error) { + refresher, err := refresh.CreateRefresherFromConfig(config) + if err != nil { return ctrl.Result{}, err } - // if certificate store is configured, return error. Only one of certificate store and key management provider can be configured - if len(certificateStoreList.Items) > 0 { - // Note: for backwards compatibility in upgrade scenarios, Ratify will only log a warning statement. - logger.Warn("Certificate Store already exists. Key management provider and certificate store should not be configured together. Please migrate to key management provider and delete certificate store.") - } - - provider, err := cutils.SpecToKeyManagementProvider(keyManagementProvider.Spec.Parameters.Raw, keyManagementProvider.Spec.Type) + err = refresher.Refresh(ctx) if err != nil { - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) return ctrl.Result{}, err } - // fetch certificates and store in map - certificates, certAttributes, err := provider.GetCertificates(ctx) - if err != nil { - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) - return ctrl.Result{}, fmt.Errorf("Error fetching certificates in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) + result, ok := refresher.GetResult().(ctrl.Result) + if !ok { + return ctrl.Result{}, fmt.Errorf("unexpected type returned from GetResult: %T", refresher.GetResult()) } + return result, nil +} - // fetch keys and store in map - keys, keyAttributes, err := provider.GetKeys(ctx) - if err != nil { - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) - return ctrl.Result{}, fmt.Errorf("Error fetching keys in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedkeymanagementproviders,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedkeymanagementproviders/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedkeymanagementproviders/finalizers,verbs=update +func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + refresherConfig := map[string]interface{}{ + "type": refresh.KubeRefresherNamespacedType, + "client": r.Client, + "request": req, } - kmp.SetCertificatesInMap(resource, certificates) - kmp.SetKeysInMap(resource, keyManagementProvider.Spec.Type, keys) - // merge certificates and keys status into one - maps.Copy(keyAttributes, certAttributes) - isFetchSuccessful = true - emptyErrorString := "" - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, emptyErrorString, lastFetchedTime, keyAttributes) - - logger.Infof("%v certificate(s) & %v key(s) fetched for key management provider %v", len(certificates), len(keys), resource) - - // returning empty result and no error to indicate we’ve successfully reconciled this object - return ctrl.Result{}, nil + return r.ReconcileWithConfig(ctx, refresherConfig) } // SetupWithManager sets up the controller with the Manager. @@ -127,46 +77,3 @@ func (r *KeyManagementProviderReconciler) SetupWithManager(mgr ctrl.Manager) err For(&configv1beta1.NamespacedKeyManagementProvider{}).WithEventFilter(pred). Complete(r) } - -// writeKMProviderStatus updates the status of the key management provider resource -func writeKMProviderStatus(ctx context.Context, r client.StatusClient, keyManagementProvider *configv1beta1.NamespacedKeyManagementProvider, logger *logrus.Entry, isSuccess bool, errorString string, operationTime metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { - if isSuccess { - updateKMProviderSuccessStatus(keyManagementProvider, &operationTime, kmProviderStatus) - } else { - updateKMProviderErrorStatus(keyManagementProvider, errorString, &operationTime) - } - if statusErr := r.Status().Update(ctx, keyManagementProvider); statusErr != nil { - logger.Error(statusErr, ",unable to update key management provider error status") - } -} - -// updateKMProviderErrorStatus updates the key management provider status with error, brief error and last fetched time -func updateKMProviderErrorStatus(keyManagementProvider *configv1beta1.NamespacedKeyManagementProvider, errorString string, operationTime *metav1.Time) { - // truncate brief error string to maxBriefErrLength - briefErr := errorString - if len(errorString) > constants.MaxBriefErrLength { - briefErr = fmt.Sprintf("%s...", errorString[:constants.MaxBriefErrLength]) - } - keyManagementProvider.Status.IsSuccess = false - keyManagementProvider.Status.Error = errorString - keyManagementProvider.Status.BriefError = briefErr - keyManagementProvider.Status.LastFetchedTime = operationTime -} - -// updateKMProviderSuccessStatus updates the key management provider status if status argument is non nil -// Success status includes last fetched time and other provider-specific properties -func updateKMProviderSuccessStatus(keyManagementProvider *configv1beta1.NamespacedKeyManagementProvider, lastOperationTime *metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { - keyManagementProvider.Status.IsSuccess = true - keyManagementProvider.Status.Error = "" - keyManagementProvider.Status.BriefError = "" - keyManagementProvider.Status.LastFetchedTime = lastOperationTime - - if kmProviderStatus != nil { - jsonString, _ := json.Marshal(kmProviderStatus) - - raw := runtime.RawExtension{ - Raw: jsonString, - } - keyManagementProvider.Status.Properties = raw - } -} diff --git a/pkg/controllers/namespaceresource/keymanagementprovider_controller_test.go b/pkg/controllers/namespaceresource/keymanagementprovider_controller_test.go index fe0728d6c..7e4717a8e 100644 --- a/pkg/controllers/namespaceresource/keymanagementprovider_controller_test.go +++ b/pkg/controllers/namespaceresource/keymanagementprovider_controller_test.go @@ -17,264 +17,144 @@ package namespaceresource import ( "context" - "fmt" + "errors" + "reflect" "testing" - configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" - "github.com/ratify-project/ratify/pkg/keymanagementprovider" - "github.com/sirupsen/logrus" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/ratify-project/ratify/internal/constants" - ctxUtils "github.com/ratify-project/ratify/internal/context" + "github.com/ratify-project/ratify/pkg/keymanagementprovider/refresh" test "github.com/ratify-project/ratify/pkg/utils" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" -) - -const ( - kmpName = "kmpName" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -// TestUpdateErrorStatus tests the updateErrorStatus method -func TestKMProviderUpdateErrorStatus(t *testing.T) { - var parametersString = "{\"certs\":{\"name\":\"certName\"}}" - var kmProviderStatus = []byte(parametersString) - - status := configv1beta1.NamespacedKeyManagementProviderStatus{ - IsSuccess: true, - Properties: runtime.RawExtension{ - Raw: kmProviderStatus, +func TestKeyManagementProviderReconciler_ReconcileWithConfig(t *testing.T) { + tests := []struct { + name string + refresherType string + createConfigError bool + refreshError bool + expectedError bool + }{ + { + name: "Successful Reconcile", + refresherType: "mockRefresher", + createConfigError: false, + refreshError: false, + expectedError: false, + }, + { + name: "Refresher Error", + refresherType: "mockRefresher", + createConfigError: false, + refreshError: true, + expectedError: true, + }, + { + name: "Invalid Refresher Type", + refresherType: "invalidRefresher", + createConfigError: true, + refreshError: false, + expectedError: true, }, } - keyManagementProvider := configv1beta1.NamespacedKeyManagementProvider{ - Status: status, - } - expectedErr := "it's a long error from unit test" - lastFetchedTime := metav1.Now() - updateKMProviderErrorStatus(&keyManagementProvider, expectedErr, &lastFetchedTime) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: "fake-name", + Namespace: "fake-namespace", + }, + } + scheme, _ := test.CreateScheme() + client := fake.NewClientBuilder().WithScheme(scheme).Build() - if keyManagementProvider.Status.IsSuccess != false { - t.Fatalf("Unexpected error, expected isSuccess to be false , actual %+v", keyManagementProvider.Status.IsSuccess) - } + r := &KeyManagementProviderReconciler{ + Client: client, + Scheme: runtime.NewScheme(), + } - if keyManagementProvider.Status.Error != expectedErr { - t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedErr, keyManagementProvider.Status.Error) - } - expectedBriedErr := fmt.Sprintf("%s...", expectedErr[:30]) - if keyManagementProvider.Status.BriefError != expectedBriedErr { - t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedBriedErr, keyManagementProvider.Status.Error) - } + refresherConfig := map[string]interface{}{ + "type": tt.refresherType, + "client": client, + "request": req, + "createConfigError": tt.createConfigError, + "refreshError": tt.refreshError, + "shouldError": tt.expectedError, + } - //make sure properties of last cached cert was not overridden - if len(keyManagementProvider.Status.Properties.Raw) == 0 { - t.Fatalf("Unexpected properties, expected %+v, got %+v", parametersString, string(keyManagementProvider.Status.Properties.Raw)) + _, err := r.ReconcileWithConfig(context.TODO(), refresherConfig) + if tt.expectedError && err == nil { + t.Errorf("Expected error, got nil") + } + if !tt.expectedError && err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) } } -// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method -func TestKMProviderUpdateSuccessStatus(t *testing.T) { - kmProviderStatus := keymanagementprovider.KeyManagementProviderStatus{} - properties := map[string]string{} - properties["Name"] = "wabbit" - properties["Version"] = "ABC" - - kmProviderStatus["Certificates"] = properties - - lastFetchedTime := metav1.Now() - - status := configv1beta1.NamespacedKeyManagementProviderStatus{ - IsSuccess: false, - Error: "error from last operation", - } - keyManagementProvider := configv1beta1.NamespacedKeyManagementProvider{ - Status: status, +func TestKeyManagementProviderReconciler_Reconcile(t *testing.T) { + req := ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: "fake-name", + Namespace: "fake-namespace", + }, } - updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, kmProviderStatus) + // Create a fake client and scheme + scheme, _ := test.CreateScheme() + client := fake.NewClientBuilder().WithScheme(scheme).Build() - if keyManagementProvider.Status.IsSuccess != true { - t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + r := &KeyManagementProviderReconciler{ + Client: client, + Scheme: runtime.NewScheme(), } - if keyManagementProvider.Status.Error != "" { - t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + // Call the Reconcile method + result, err := r.Reconcile(context.TODO(), req) + if err != nil { + t.Errorf("Unexpected error: %v", err) } - //make sure properties of last cached cert was updated - if len(keyManagementProvider.Status.Properties.Raw) == 0 { - t.Fatalf("Properties should not be empty") + // Check the result + expectedResult := ctrl.Result{} + if !reflect.DeepEqual(result, expectedResult) { + t.Errorf("Expected result %v, got %v", expectedResult, result) } } -// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method with empty properties -func TestKMProviderUpdateSuccessStatus_emptyProperties(t *testing.T) { - lastFetchedTime := metav1.Now() - status := configv1beta1.NamespacedKeyManagementProviderStatus{ - IsSuccess: false, - Error: "error from last operation", - } - keyManagementProvider := configv1beta1.NamespacedKeyManagementProvider{ - Status: status, - } - - updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, nil) - - if keyManagementProvider.Status.IsSuccess != true { - t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) - } - - if keyManagementProvider.Status.Error != "" { - t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) - } - - //make sure properties of last cached cert was updated - if len(keyManagementProvider.Status.Properties.Raw) != 0 { - t.Fatalf("Properties should be empty") - } +type MockRefresher struct { + Results ctrl.Result + CreateConfigError bool + RefreshError bool + ShouldError bool } -func TestWriteKMProviderStatus(t *testing.T) { - logger := logrus.WithContext(context.Background()) - lastFetchedTime := metav1.Now() - testCases := []struct { - name string - isSuccess bool - kmProvider *configv1beta1.NamespacedKeyManagementProvider - errString string - reconciler client.StatusClient - }{ - { - name: "success status", - isSuccess: true, - errString: "", - kmProvider: &configv1beta1.NamespacedKeyManagementProvider{}, - reconciler: &test.MockStatusClient{}, - }, - { - name: "error status", - isSuccess: false, - kmProvider: &configv1beta1.NamespacedKeyManagementProvider{}, - errString: "a long error string that exceeds the max length of 30 characters", - reconciler: &test.MockStatusClient{}, - }, - { - name: "status update failed", - isSuccess: true, - kmProvider: &configv1beta1.NamespacedKeyManagementProvider{}, - reconciler: &test.MockStatusClient{ - UpdateFailed: true, - }, - }, +func (mr *MockRefresher) Refresh(_ context.Context) error { + if mr.RefreshError { + return errors.New("refresh error") } + return nil +} - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - writeKMProviderStatus(context.Background(), tc.reconciler, tc.kmProvider, logger, tc.isSuccess, tc.errString, lastFetchedTime, nil) - - if tc.kmProvider.Status.IsSuccess != tc.isSuccess { - t.Fatalf("Expected isSuccess to be %+v , actual %+v", tc.isSuccess, tc.kmProvider.Status.IsSuccess) - } - - if tc.kmProvider.Status.Error != tc.errString { - t.Fatalf("Expected Error to be %+v , actual %+v", tc.errString, tc.kmProvider.Status.Error) - } - }) - } +func (mr *MockRefresher) GetResult() interface{} { + return ctrl.Result{} } -func TestKMPReconcile(t *testing.T) { - tests := []struct { - name string - description string - provider *configv1beta1.NamespacedKeyManagementProvider - req *reconcile.Request - expectedErr bool - expectedKMPCount int - }{ - { - name: "nonexistent KMP", - description: "Reconciling a non-existent KMP CR, it should be deleted from maps", - req: &reconcile.Request{ - NamespacedName: types.NamespacedName{Name: "nonexistent"}, - }, - provider: &configv1beta1.NamespacedKeyManagementProvider{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: kmpName, - }, - Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ - Type: "inline", - }, - }, - expectedErr: false, - expectedKMPCount: 0, - }, - { - name: "invalid params", - description: "Received invalid parameters of the KMP Spec, it should fail the reconcile and return an error", - provider: &configv1beta1.NamespacedKeyManagementProvider{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: kmpName, - }, - Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ - Type: "inline", - }, - }, - expectedErr: true, - expectedKMPCount: 0, - }, - { - name: "valid params", - description: "Received a valid KMP manifest, it should be added to the cert map", - provider: &configv1beta1.NamespacedKeyManagementProvider{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: kmpName, - }, - Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ - Type: "inline", - Parameters: runtime.RawExtension{ - Raw: []byte(`{"type": "inline", "contentType": "certificate", "value": "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n"}`), - }, - }, - }, - expectedErr: false, - expectedKMPCount: 1, - }, +func (mr *MockRefresher) Create(config map[string]interface{}) (refresh.Refresher, error) { + createConfigError := config["shouldError"].(bool) + refreshError := config["refreshError"].(bool) + if createConfigError { + return nil, errors.New("create error") } + return &MockRefresher{ + CreateConfigError: createConfigError, + RefreshError: refreshError, + }, nil +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - scheme, _ := test.CreateScheme() - client := fake.NewClientBuilder().WithScheme(scheme) - client.WithObjects(tt.provider) - r := &KeyManagementProviderReconciler{ - Scheme: scheme, - Client: client.Build(), - } - var req reconcile.Request - if tt.req != nil { - req = *tt.req - } else { - req = reconcile.Request{ - NamespacedName: test.KeyFor(tt.provider), - } - } - - _, err := r.Reconcile(context.Background(), req) - if tt.expectedErr != (err != nil) { - t.Fatalf("Reconcile() expected error %v, actual %v", tt.expectedErr, err) - } - ctx := ctxUtils.SetContextWithNamespace(context.Background(), testNamespace) - certs, _ := keymanagementprovider.GetCertificatesFromMap(ctx, testNamespace+constants.NamespaceSeperator+kmpName) - if len(certs) != tt.expectedKMPCount { - t.Fatalf("Cert map expected size %v, actual %v", tt.expectedKMPCount, len(certs)) - } - }) - } +func init() { + refresh.Register("mockRefresher", &MockRefresher{}) } diff --git a/pkg/keymanagementprovider/azurekeyvault/provider.go b/pkg/keymanagementprovider/azurekeyvault/provider.go index f24114751..c433117f9 100644 --- a/pkg/keymanagementprovider/azurekeyvault/provider.go +++ b/pkg/keymanagementprovider/azurekeyvault/provider.go @@ -193,6 +193,10 @@ func (s *akvKMProvider) GetKeys(ctx context.Context) (map[keymanagementprovider. return keysMap, getStatusMap(keysStatus, types.KeysStatus), nil } +func (s *akvKMProvider) IsRefreshable() bool { + return true +} + // azure keyvault provider certificate/key status is a map from "certificates" key or "keys" key to an array of key management provider status func getStatusMap(statusMap []map[string]string, contentType string) keymanagementprovider.KeyManagementProviderStatus { status := keymanagementprovider.KeyManagementProviderStatus{} diff --git a/pkg/keymanagementprovider/azurekeyvault/provider_test.go b/pkg/keymanagementprovider/azurekeyvault/provider_test.go index 9d134e59f..ce95d24a7 100644 --- a/pkg/keymanagementprovider/azurekeyvault/provider_test.go +++ b/pkg/keymanagementprovider/azurekeyvault/provider_test.go @@ -238,6 +238,26 @@ func TestGetKeys(t *testing.T) { assert.Nil(t, keyStatus) } +func TestIsRefreshable(t *testing.T) { + factory := &akvKMProviderFactory{} + config := config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "clientID": "clientid", + "certificates": []map[string]interface{}{ + { + "name": "cert1", + "version": "", + }, + }, + } + + provider, _ := factory.Create("v1", config, "") + if provider.IsRefreshable() != true { + t.Fatalf("expected true, got false") + } +} + // TestGetStatusMap tests the getStatusMap function func TestGetStatusMap(t *testing.T) { certsStatus := []map[string]string{} diff --git a/pkg/keymanagementprovider/inline/provider.go b/pkg/keymanagementprovider/inline/provider.go index 2bd6b83ef..81740a750 100644 --- a/pkg/keymanagementprovider/inline/provider.go +++ b/pkg/keymanagementprovider/inline/provider.go @@ -114,3 +114,7 @@ func (s *inlineKMProvider) GetCertificates(_ context.Context) (map[keymanagement func (s *inlineKMProvider) GetKeys(_ context.Context) (map[keymanagementprovider.KMPMapKey]crypto.PublicKey, keymanagementprovider.KeyManagementProviderStatus, error) { return s.keys, nil, nil } + +func (s *inlineKMProvider) IsRefreshable() bool { + return false +} diff --git a/pkg/keymanagementprovider/inline/provider_test.go b/pkg/keymanagementprovider/inline/provider_test.go index 68b461523..7cd56f67c 100644 --- a/pkg/keymanagementprovider/inline/provider_test.go +++ b/pkg/keymanagementprovider/inline/provider_test.go @@ -116,3 +116,40 @@ func TestGetCertificates(t *testing.T) { }) } } + +func TestGetKeys(t *testing.T) { + factory := &inlineKMProviderFactory{} + config := config.KeyManagementProviderConfig{ + "type": "inline", + "contentType": "key", + "value": `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEozC27QupU+1GvAL0tqR7bT3Vpyyf +OSeWVmPjy6J5x8+6OIpmTs8PKQB1vTF0gErwa1gS/QaOElLaxDKy0GS9Jg== +-----END PUBLIC KEY-----`, + } + + provider, err := factory.Create("v1.0", config, "") + if err != nil { + t.Fatalf("failed to create provider: %v", err) + } + keys, _, _ := provider.GetKeys(context.TODO()) + if len(keys) != 1 { + t.Fatalf("expected 1 key, got %d", len(keys)) + } +} + +func TestIsRefreshable(t *testing.T) { + config := config.KeyManagementProviderConfig{ + "type": "inline", + "contentType": "certificate", + "value": "-----BEGIN CERTIFICATE-----\nMIIEeDCCAmCgAwIBAgIUbztxbi/gSPMZGN53oZQZW1h/lbAwDQYJKoZIhvcNAQEL\nBQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25k\nMRMwEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMM\nDmNhLmV4YW1wbGUuY29tMB4XDTIzMDIwMTIyNTMxMFoXDTI0MDIwMTIyNTMxMFow\nbTEZMBcGA1UEAxMQbGVhZi5leGFtcGxlLmNvbTEPMA0GA1UECxMGTXkgT3JnMRMw\nEQYDVQQKEwpNeSBDb21wYW55MRAwDgYDVQQHEwdSZWRtb25kMQswCQYDVQQIEwJX\nQTELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs\nMixf+WT34edYXs2c80zOg4Z/cxOVHU05gywjuISeaP+KS+Joc3emgbub1t5dPclk\nieIwrj3Olk3tvkrPiarOJIcrNR2zfBmQAufR4AUjoc4n1GQSp/voGgw1Hvh0wTkO\nYjhzLomrF242Ond8WTVO3Vq6/tfApfZMFM59eK9LMBkuvwTV4NeLnEnPvpLAoAvV\n9ZvCu7FuQ849R93Aoag2bZc3Tc3UCbahoJs9rTE/rnAqOhJWMGv2J1Y2Wu2eIvkD\n2uCmcVlY+7owG3TwLHTuIOBFl/5MXMvfR+B7yp1OkG23rTwwuSEBlMhYRzJFvssv\n8FX0sea7zhIg5dtoRjIlAgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG\n9w0BAQsFAAOCAgEANRUu+9aBAuRf3OsdUWJflMAvuyzREp3skWSOUs4dw0MhcB6x\nb7BSyNdrBgPImLBpqYzNU6IT2eIlLXrYnKLehvPyZQx7LHvIeompA09aKMFFesqi\ngVoW5GRtp3qL3oNuuZJ80r/uKlB6Cj51TWqUbLcctBGHX7TWxFeWmFRnN0Bki00U\nJW/ElaTsr4GB+ltgZM+5USUqSNQqTa8t3d+vH6oVikyV1oYunM41xAfiRZtID04z\no15sLSkWTjavfmZ3+NjllipXFY2tnLqymCcObgdKtJHmTMFSDRngDjY+3+RVj4EY\npNaCCCepvtmXz1C5f06tlgY4ofaautJuAL7K93p/Q9ZcsIhmYWkCUZ0dkWq+eMdT\n9/lB9rQHbrDTaRxEQNIUezFMQEBxR9eC5JQfpw98LobAgA3r4vizQjQPsN0UZ54h\ncAHiyoo1VeckkotXaToRsoLjixPO9Fmss4H3urJTLpcU0drbVoG3emNh4K289vgR\nrjV11TenqvvR3+jJ2AX2ewSsF25m0afheZbrq2ZtyITPAbOqwMwTbTOvJT3HUztt\nhUP3qwsKNPR/hF3FSqZewiYOSqJi5Dk28Vd6mUEQzZa/Ma9RpBR+BAmfgH3Z9gX5\n0TqmAVQn1P8yh+bhEjiNa20bTJ+y5vQ9OrA7fiQ+6vpZCio4NFiEbYK4UBI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFxzCCA6+gAwIBAgIUY1fnGcYJFIGNk8fevKdGtOuZ5vcwDQYJKoZIhvcNAQEL\nBQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25k\nMRMwEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMM\nDmNhLmV4YW1wbGUuY29tMB4XDTIzMDIwMTIyNTIwN1oXDTMzMDEyOTIyNTIwN1ow\nazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25kMRMw\nEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMMDmNh\nLmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtQXd\n+qySVVMHx7iGz9xRdpDKb+zATK3asMFnMXWBn0MCkcOMJvhjajA8yrCpfMudjiW0\nReQ6xcjEfnJqkzwxrK7tPE9cQ6JzQGCxsmscRzKf1NoJL2ske5xBteKuJhSfZ9la\ncIZn/EU2F6eMAl9U4Y+ncIIrl4UoB5H/AJJj62WMl5QAvzXXwCBwlLHQe4/T3Axu\n9xmD3HTC7iQExOUFLdJx86fK3ym0futi1RgOUgD+OrnyDEIkD8mGxffPYPgszS71\nUuJX+NTsLZ/JW3ER/PMAPnBsMsMTTxEIGnrp1CXf2RnwQnDHVsxZFMgkLTS6dksT\nTGevnulTNtVvSKsZ7MpzE01j7zDie4V4dQJzBJMktbeVq9KRoPIEX0WcKpg8bGS8\nd5p5pr+Lu/NOv6ur+av8M649qCPwJAv5i2P6ggT4YMNtY0wMD2kjcHJ9/l2gYpZj\n3DG0Hy3Xo8uKUmTSC7iGhLsSjleNhJkKyh3RCsuMKB9juE4qeXPoaoPWBIuarbkq\neVVZJu2PlgN8UcxM/ym+9GNJIfNJ18WGWm+5P3IDvfBbJ39yvzZlG2czju4BUzYn\nlPOHA/Z18TxZhlPrPVnyKSVbeg7sW17yMUI2LCeaFIOYdIFvM09RaCyLIGrQwhpe\nkLh56xXk702oNHaLxyh/v5kz8EyxnpXIlDntis0CAwEAAaNjMGEwHQYDVR0OBBYE\nFCESfoahHMx7GyBdTBARen7mc37nMB8GA1UdIwQYMBaAFCESfoahHMx7GyBdTBAR\nen7mc37nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBCwUAA4ICAQBst1MbRJd3FLSY36qaNuDhGncvcXIcYqP2A/SXVhjuAhVOTsrE\nejDYBSkjfxCgoA3LZnQLTcbcPwRL5dTBEvqBqzbyArOjc+G3kbjOeuZYv09M8kUU\nYG1bVnxXV17GMk7zcBUnr1iwnp4E0pzB9gTv+Jv1oV+EtAHe5QOTOmW9mm2SUXbH\nmIya+KlzkIgVJJ8kiGOyXsr8i4wpyDXZf720tqTzPQFTf6rUXo9PhYzOWrrj8c7V\n+bmJurV3XkgvdOiwNase17wXUG9Ad8FhVYpUicq3Csfx5M87IXUIlx51AxaOQK3G\n3skyJyAm8R8pHzhcsEuVV7bGZlbFPZeWAHpbIEwpKHlLoN6qMINk2kEbcVahL2Mu\nXBUcvJdO2LbmEvfS+1imr32YbJ5Ufetru+G4mwAp6a0P6u5FU8lbE1ZoFHhflsVq\nErvOcRKhKjAim4iwIVGS55Xyx1IpF7YSTYpL89vlMmaJsssEoCQAkf+lxmC3eEuy\n2kBu3QB3cUHZXJK+01krC5MqeHcEVuc/fbNcTsCBp+RYXNRjYsp6IzGjqltUfInE\n3Tywg3P0ZLPO06WLNjNeAFZw6T8yV5gLcTJ1xc5pEf4UiY1uCf4NmDpeWhT+vvto\n2AxC/+7x7EkEfZnYiD6tcyHyY+iuroptws8lc5wRis859kydnq3vtxbXPQ==\n-----END CERTIFICATE-----\n", + } + factory := &inlineKMProviderFactory{} + provider, err := factory.Create("v1.0", config, "") + if err != nil { + t.Fatalf("failed to create provider: %v", err) + } + if provider.IsRefreshable() != false { + t.Fatalf("expected false") + } +} diff --git a/pkg/keymanagementprovider/keymanagementprovider.go b/pkg/keymanagementprovider/keymanagementprovider.go index 714deda94..3c9c6957c 100644 --- a/pkg/keymanagementprovider/keymanagementprovider.go +++ b/pkg/keymanagementprovider/keymanagementprovider.go @@ -54,6 +54,8 @@ type KeyManagementProvider interface { GetCertificates(ctx context.Context) (map[KMPMapKey][]*x509.Certificate, KeyManagementProviderStatus, error) // Returns an array of keys and the provider specific key attributes GetKeys(ctx context.Context) (map[KMPMapKey]crypto.PublicKey, KeyManagementProviderStatus, error) + // Returns if the provider supports refreshing of certificates/keys + IsRefreshable() bool } // static concurrency-safe map to store certificates fetched from key management provider diff --git a/pkg/keymanagementprovider/mocks/client.go b/pkg/keymanagementprovider/mocks/client.go new file mode 100644 index 000000000..f24c19029 --- /dev/null +++ b/pkg/keymanagementprovider/mocks/client.go @@ -0,0 +1,32 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package mocks + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type TestClient struct { + client.Client +} + +func (m TestClient) Get(_ context.Context, _ types.NamespacedName, _ client.Object, _ ...client.GetOption) error { + return fmt.Errorf("error") +} diff --git a/pkg/keymanagementprovider/mocks/factory.go b/pkg/keymanagementprovider/mocks/factory.go new file mode 100644 index 000000000..0230582e2 --- /dev/null +++ b/pkg/keymanagementprovider/mocks/factory.go @@ -0,0 +1,33 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package mocks + +import ( + "crypto" + "crypto/x509" + + "github.com/ratify-project/ratify/pkg/keymanagementprovider" + "github.com/ratify-project/ratify/pkg/keymanagementprovider/config" +) + +type TestKeyManagementProviderFactory struct { +} + +func (f *TestKeyManagementProviderFactory) Create(_ string, _ config.KeyManagementProviderConfig, _ string) (keymanagementprovider.KeyManagementProvider, error) { + var certMap map[keymanagementprovider.KMPMapKey][]*x509.Certificate + var keyMap map[keymanagementprovider.KMPMapKey]crypto.PublicKey + return &TestKeyManagementProvider{certificates: certMap, keys: keyMap}, nil +} diff --git a/pkg/keymanagementprovider/mocks/types.go b/pkg/keymanagementprovider/mocks/types.go index 829b08e52..246de21ba 100644 --- a/pkg/keymanagementprovider/mocks/types.go +++ b/pkg/keymanagementprovider/mocks/types.go @@ -37,3 +37,7 @@ func (c *TestKeyManagementProvider) GetCertificates(_ context.Context) (map[keym func (c *TestKeyManagementProvider) GetKeys(_ context.Context) (map[keymanagementprovider.KMPMapKey]crypto.PublicKey, keymanagementprovider.KeyManagementProviderStatus, error) { return c.keys, c.status, c.err } + +func (c *TestKeyManagementProvider) IsRefreshable() bool { + return true +} diff --git a/pkg/keymanagementprovider/refresh/factory.go b/pkg/keymanagementprovider/refresh/factory.go new file mode 100644 index 000000000..186951c16 --- /dev/null +++ b/pkg/keymanagementprovider/refresh/factory.go @@ -0,0 +1,53 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package refresh + +import "fmt" + +var refresherFactories = make(map[string]RefresherFactory) + +type RefresherFactory interface { + // Create creates a new instance of the refresher using the provided configuration + Create(config map[string]interface{}) (Refresher, error) +} + +// Refresher is an interface that defines methods to be implemented by a each refresher +func Register(name string, factory RefresherFactory) { + if factory == nil { + panic("refresher factory cannot be nil") + } + _, registered := refresherFactories[name] + if registered { + panic(fmt.Sprintf("refresher factory named %s already registered", name)) + } + refresherFactories[name] = factory +} + +// CreateRefresherFromConfig creates a new instance of the refresher using the provided configuration +func CreateRefresherFromConfig(refresherConfig map[string]interface{}) (Refresher, error) { + refresherType, ok := refresherConfig["type"].(string) + if !ok { + return nil, fmt.Errorf("refresher type is not a string") + } + if !ok || refresherType == "" { + return nil, fmt.Errorf("refresher type cannot be empty") + } + factory, ok := refresherFactories[refresherType] + if !ok { + return nil, fmt.Errorf("refresher factory with name %s not found", refresherType) + } + return factory.Create(refresherConfig) +} diff --git a/pkg/keymanagementprovider/refresh/factory_test.go b/pkg/keymanagementprovider/refresh/factory_test.go new file mode 100644 index 000000000..a4a267cc2 --- /dev/null +++ b/pkg/keymanagementprovider/refresh/factory_test.go @@ -0,0 +1,119 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package refresh + +import ( + "context" + "testing" +) + +type MockRefresher struct{} + +func (f *MockRefresher) Create(_ map[string]interface{}) (Refresher, error) { + return &MockRefresher{}, nil +} + +func (f *MockRefresher) Refresh(_ context.Context) error { + return nil +} + +func (f *MockRefresher) GetResult() interface{} { + return nil +} + +func TestRefreshFactory_Create(t *testing.T) { + Register("mockRefresher", &MockRefresher{}) + refresherConfig := map[string]interface{}{ + "type": "mockRefresher", + } + factory := refresherFactories["mockRefresher"] + refresher, err := factory.Create(refresherConfig) + // refresher, err := CreateRefresherFromConfig(refresherConfig) + if _, ok := refresher.(*MockRefresher); !ok { + t.Errorf("Expected refresher to be of type MockRefresher, got %v", refresher) + } + if err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestRegister_InvalidFactory(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic, got nil") + } + }() + + Register("invalidRefresher", nil) +} + +func TestRegister_DuplicateFactory(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic, got nil") + } + }() + + Register("duplicateRefresher", &MockRefresher{}) + Register("duplicateRefresher", &MockRefresher{}) +} + +func TestRegister_ValidFactory(t *testing.T) { + refresherFactories = make(map[string]RefresherFactory) + Register("validRefresher", &MockRefresher{}) + if len(refresherFactories) != 1 { + t.Errorf("Expected 1 factory to be registered, got %d", len(refresherFactories)) + } +} + +func TestCreateRefresherFromConfig(t *testing.T) { + Register("mockRefresher", &MockRefresher{}) + tests := []struct { + name string + refresherType string + expectedError bool + }{ + { + name: "Valid Refresher Type", + refresherType: "mockRefresher", + expectedError: false, + }, + { + name: "Invalid Refresher Type", + refresherType: "invalidRefresher", + expectedError: true, + }, + { + name: "Empty Refresher Type", + refresherType: "", + expectedError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + refresherConfig := map[string]interface{}{ + "type": tt.refresherType, + } + _, err := CreateRefresherFromConfig(refresherConfig) + if tt.expectedError && err == nil { + t.Errorf("Expected error, got nil") + } + if !tt.expectedError && err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + } +} diff --git a/pkg/keymanagementprovider/refresh/kubeRefresh.go b/pkg/keymanagementprovider/refresh/kubeRefresh.go new file mode 100644 index 000000000..21098d4a5 --- /dev/null +++ b/pkg/keymanagementprovider/refresh/kubeRefresh.go @@ -0,0 +1,210 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package refresh + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "time" + + configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" + "github.com/ratify-project/ratify/internal/constants" + cutils "github.com/ratify-project/ratify/pkg/controllers/utils" + kmp "github.com/ratify-project/ratify/pkg/keymanagementprovider" + "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type KubeRefresher struct { + client.Client + Request ctrl.Request + Result ctrl.Result +} + +// Register registers the kubeRefresher factory +func init() { + Register(KubeRefresherType, &KubeRefresher{}) +} + +// Refresh the certificates/keys for the key management provider by calling the GetCertificates and GetKeys methods +func (kr *KubeRefresher) Refresh(ctx context.Context) error { + logger := logrus.WithContext(ctx) + + var resource = kr.Request.Name + var keyManagementProvider configv1beta1.KeyManagementProvider + + logger.Infof("reconciling cluster key management provider '%v'", resource) + + if err := kr.Get(ctx, kr.Request.NamespacedName, &keyManagementProvider); err != nil { + if apierrors.IsNotFound(err) { + logger.Infof("deletion detected, removing key management provider %v", resource) + kmp.DeleteCertificatesFromMap(resource) + kmp.DeleteKeysFromMap(resource) + } else { + logger.Error(err, "unable to fetch key management provider") + } + + kr.Result = ctrl.Result{} + + return client.IgnoreNotFound(err) + } + + lastFetchedTime := metav1.Now() + isFetchSuccessful := false + + // get certificate store list to check if certificate store is configured + // TODO: remove check in v2.0.0+ + var certificateStoreList configv1beta1.CertificateStoreList + if err := kr.List(ctx, &certificateStoreList); err != nil { + logger.Error(err, "unable to list certificate stores") + kr.Result = ctrl.Result{} + return err + } + + if len(certificateStoreList.Items) > 0 { + // Note: for backwards compatibility in upgrade scenarios, Ratify will only log a warning statement. + logger.Warn("Certificate Store already exists. Key management provider and certificate store should not be configured together. Please migrate to key management provider and delete certificate store.") + } + + provider, err := cutils.SpecToKeyManagementProvider(keyManagementProvider.Spec.Parameters.Raw, keyManagementProvider.Spec.Type) + if err != nil { + writeKMProviderStatus(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + kr.Request = ctrl.Request{} + return err + } + + // fetch certificates and store in map + certificates, certAttributes, err := provider.GetCertificates(ctx) + if err != nil { + writeKMProviderStatus(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + kr.Request = ctrl.Request{} + return fmt.Errorf("error fetching certificates in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) + } + + // fetch keys and store in map + keys, keyAttributes, err := provider.GetKeys(ctx) + if err != nil { + writeKMProviderStatus(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + kr.Request = ctrl.Request{} + return fmt.Errorf("error fetching keys in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) + } + kmp.SetCertificatesInMap(resource, certificates) + kmp.SetKeysInMap(resource, keyManagementProvider.Spec.Type, keys) + // merge certificates and keys status into one + maps.Copy(keyAttributes, certAttributes) + isFetchSuccessful = true + emptyErrorString := "" + writeKMProviderStatus(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, emptyErrorString, lastFetchedTime, keyAttributes) + + logger.Infof("%v certificate(s) & %v key(s) fetched for key management provider %v", len(certificates), len(keys), resource) + + // returning empty result and no error to indicate we’ve successfully reconciled this object + // will not reconcile again unless resource is recreated + if !provider.IsRefreshable() { + kr.Request = ctrl.Request{} + return nil + } + + // if interval is not set, disable refresh + if keyManagementProvider.Spec.RefreshInterval == "" { + kr.Result = ctrl.Result{} + return nil + } + // resource is refreshable, requeue after interval + intervalDuration, err := time.ParseDuration(keyManagementProvider.Spec.RefreshInterval) + if err != nil { + logger.Error(err, "unable to parse interval duration") + kr.Result = ctrl.Result{} + return err + } + + logger.Info("Reconciled KeyManagementProvider", "intervalDuration", intervalDuration) + kr.Result = ctrl.Result{RequeueAfter: intervalDuration} + + return nil +} + +// GetResult returns the result of the refresh as a ctrl.Result +func (kr *KubeRefresher) GetResult() interface{} { + return kr.Result +} + +// Create creates a new KubeRefresher instance +func (kr *KubeRefresher) Create(config map[string]interface{}) (Refresher, error) { + client, ok := config["client"].(client.Client) + if !ok { + return nil, fmt.Errorf("client is required in config") + } + + request, ok := config["request"].(ctrl.Request) + if !ok { + return nil, fmt.Errorf("request is required in config") + } + + return &KubeRefresher{ + Client: client, + Request: request, + }, nil +} + +func writeKMProviderStatus(ctx context.Context, r client.StatusClient, keyManagementProvider *configv1beta1.KeyManagementProvider, logger *logrus.Entry, isSuccess bool, errorString string, operationTime metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { + if isSuccess { + updateKMProviderSuccessStatus(keyManagementProvider, &operationTime, kmProviderStatus) + } else { + updateKMProviderErrorStatus(keyManagementProvider, errorString, &operationTime) + } + if statusErr := r.Status().Update(ctx, keyManagementProvider); statusErr != nil { + logger.Error(statusErr, ",unable to update key management provider error status") + } +} + +// updateKMProviderErrorStatus updates the key management provider status with error, brief error and last fetched time +func updateKMProviderErrorStatus(keyManagementProvider *configv1beta1.KeyManagementProvider, errorString string, operationTime *metav1.Time) { + // truncate brief error string to maxBriefErrLength + briefErr := errorString + if len(errorString) > constants.MaxBriefErrLength { + briefErr = fmt.Sprintf("%s...", errorString[:constants.MaxBriefErrLength]) + } + keyManagementProvider.Status.IsSuccess = false + keyManagementProvider.Status.Error = errorString + keyManagementProvider.Status.BriefError = briefErr + keyManagementProvider.Status.LastFetchedTime = operationTime +} + +// updateKMProviderSuccessStatus updates the key management provider status if status argument is non nil +// Success status includes last fetched time and other provider-specific properties +func updateKMProviderSuccessStatus(keyManagementProvider *configv1beta1.KeyManagementProvider, lastOperationTime *metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { + keyManagementProvider.Status.IsSuccess = true + keyManagementProvider.Status.Error = "" + keyManagementProvider.Status.BriefError = "" + keyManagementProvider.Status.LastFetchedTime = lastOperationTime + + if kmProviderStatus != nil { + jsonString, _ := json.Marshal(kmProviderStatus) + + raw := runtime.RawExtension{ + Raw: jsonString, + } + keyManagementProvider.Status.Properties = raw + } +} diff --git a/pkg/keymanagementprovider/refresh/kubeRefreshNamedspaced_test.go b/pkg/keymanagementprovider/refresh/kubeRefreshNamedspaced_test.go new file mode 100644 index 000000000..884d94fe3 --- /dev/null +++ b/pkg/keymanagementprovider/refresh/kubeRefreshNamedspaced_test.go @@ -0,0 +1,376 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package refresh + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" + "github.com/ratify-project/ratify/pkg/keymanagementprovider" + "github.com/ratify-project/ratify/pkg/keymanagementprovider/mocks" + test "github.com/ratify-project/ratify/pkg/utils" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestKubeRefresherNamespaced_Refresh(t *testing.T) { + tests := []struct { + name string + provider *configv1beta1.NamespacedKeyManagementProvider + request ctrl.Request + mockClient bool + expectedResult ctrl.Result + expectedError bool + }{ + { + name: "Non-refreshable", + provider: &configv1beta1.NamespacedKeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ + Type: "inline", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"type": "inline", "contentType": "certificate", "value": "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{}, + expectedError: false, + }, + { + name: "Disabled", + provider: &configv1beta1.NamespacedKeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ + Type: "test-kmp", + RefreshInterval: "", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{}, + expectedError: false, + }, + { + name: "Refreshable", + provider: &configv1beta1.NamespacedKeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ + Type: "test-kmp", + RefreshInterval: "1m", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedError: false, + }, + { + name: "Invalid Interval", + provider: &configv1beta1.NamespacedKeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ + Type: "", + RefreshInterval: "1mm", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{}, + expectedError: true, + }, + { + name: "IsNotFound", + provider: &configv1beta1.NamespacedKeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.NamespacedKeyManagementProviderSpec{ + Type: "", + RefreshInterval: "", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + expectedResult: ctrl.Result{}, + expectedError: false, + }, + { + name: "UnableToFetchKMP", + mockClient: true, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var client client.Client + + if tt.mockClient { + client = mocks.TestClient{} + } else { + scheme, _ := test.CreateScheme() + client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.provider).Build() + } + + kr := &KubeRefresherNamespaced{ + Client: client, + Request: tt.request, + } + err := kr.Refresh(context.Background()) + result := kr.GetResult() + if !reflect.DeepEqual(result, tt.expectedResult) { + t.Fatalf("Expected nil but got %v with error %v", result, err) + } + if tt.expectedError && err == nil { + t.Fatalf("Expected error but got nil") + } + }) + } +} + +func TestKubeRefresherNamespaced_Create(t *testing.T) { + tests := []struct { + name string + config map[string]interface{} + expectedError bool + }{ + { + name: "Success", + config: map[string]interface{}{ + "client": &mocks.TestClient{}, + "request": ctrl.Request{}, + }, + expectedError: false, + }, + { + name: "ClientMissing", + config: map[string]interface{}{ + "request": ctrl.Request{}, + }, + expectedError: true, + }, + { + name: "RequestMissing", + config: map[string]interface{}{ + "client": &mocks.TestClient{}, + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kr := &KubeRefresherNamespaced{} + _, err := kr.Create(tt.config) + if tt.expectedError && err == nil { + t.Fatalf("Expected error but got nil") + } + }) + } +} + +func TestKMProviderUpdateErrorStatusNamespaced(t *testing.T) { + var parametersString = "{\"certs\":{\"name\":\"certName\"}}" + var kmProviderStatus = []byte(parametersString) + + status := configv1beta1.NamespacedKeyManagementProviderStatus{ + IsSuccess: true, + Properties: runtime.RawExtension{ + Raw: kmProviderStatus, + }, + } + keyManagementProvider := configv1beta1.NamespacedKeyManagementProvider{ + Status: status, + } + expectedErr := "it's a long error from unit test" + lastFetchedTime := metav1.Now() + updateKMProviderErrorStatusNamespaced(&keyManagementProvider, expectedErr, &lastFetchedTime) + + if keyManagementProvider.Status.IsSuccess != false { + t.Fatalf("Unexpected error, expected isSuccess to be false , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != expectedErr { + t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedErr, keyManagementProvider.Status.Error) + } + expectedBriedErr := fmt.Sprintf("%s...", expectedErr[:30]) + if keyManagementProvider.Status.BriefError != expectedBriedErr { + t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedBriedErr, keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was not overridden + if len(keyManagementProvider.Status.Properties.Raw) == 0 { + t.Fatalf("Unexpected properties, expected %+v, got %+v", parametersString, string(keyManagementProvider.Status.Properties.Raw)) + } +} + +func TestKMProviderUpdateSuccessStatusNamespaced(t *testing.T) { + kmProviderStatus := keymanagementprovider.KeyManagementProviderStatus{} + properties := map[string]string{} + properties["Name"] = "wabbit" + properties["Version"] = "ABC" + + kmProviderStatus["Certificates"] = properties + + lastFetchedTime := metav1.Now() + + status := configv1beta1.NamespacedKeyManagementProviderStatus{ + IsSuccess: false, + Error: "error from last operation", + } + keyManagementProvider := configv1beta1.NamespacedKeyManagementProvider{ + Status: status, + } + + updateKMProviderSuccessStatusNamespaced(&keyManagementProvider, &lastFetchedTime, kmProviderStatus) + + if keyManagementProvider.Status.IsSuccess != true { + t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != "" { + t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was updated + if len(keyManagementProvider.Status.Properties.Raw) == 0 { + t.Fatalf("Properties should not be empty") + } +} + +func TestKMProviderUpdateSuccessStatusNamespaced_emptyProperties(t *testing.T) { + lastFetchedTime := metav1.Now() + status := configv1beta1.NamespacedKeyManagementProviderStatus{ + IsSuccess: false, + Error: "error from last operation", + } + keyManagementProvider := configv1beta1.NamespacedKeyManagementProvider{ + Status: status, + } + + updateKMProviderSuccessStatusNamespaced(&keyManagementProvider, &lastFetchedTime, nil) + + if keyManagementProvider.Status.IsSuccess != true { + t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != "" { + t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was updated + if len(keyManagementProvider.Status.Properties.Raw) != 0 { + t.Fatalf("Properties should be empty") + } +} + +func TestWriteKMProviderStatusNamespaced(t *testing.T) { + logger := logrus.WithContext(context.Background()) + lastFetchedTime := metav1.Now() + testCases := []struct { + name string + isSuccess bool + kmProvider *configv1beta1.NamespacedKeyManagementProvider + errString string + reconciler client.StatusClient + }{ + { + name: "success status", + isSuccess: true, + errString: "", + kmProvider: &configv1beta1.NamespacedKeyManagementProvider{}, + reconciler: &test.MockStatusClient{}, + }, + { + name: "error status", + isSuccess: false, + kmProvider: &configv1beta1.NamespacedKeyManagementProvider{}, + errString: "a long error string that exceeds the max length of 30 characters", + reconciler: &test.MockStatusClient{}, + }, + { + name: "status update failed", + isSuccess: true, + kmProvider: &configv1beta1.NamespacedKeyManagementProvider{}, + reconciler: &test.MockStatusClient{ + UpdateFailed: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + writeKMProviderStatusNamespaced(context.Background(), tc.reconciler, tc.kmProvider, logger, tc.isSuccess, tc.errString, lastFetchedTime, nil) + + if tc.kmProvider.Status.IsSuccess != tc.isSuccess { + t.Fatalf("Expected isSuccess to be %+v , actual %+v", tc.isSuccess, tc.kmProvider.Status.IsSuccess) + } + + if tc.kmProvider.Status.Error != tc.errString { + t.Fatalf("Expected Error to be %+v , actual %+v", tc.errString, tc.kmProvider.Status.Error) + } + }) + } +} diff --git a/pkg/keymanagementprovider/refresh/kubeRefreshNamespaced.go b/pkg/keymanagementprovider/refresh/kubeRefreshNamespaced.go new file mode 100644 index 000000000..ab72e1076 --- /dev/null +++ b/pkg/keymanagementprovider/refresh/kubeRefreshNamespaced.go @@ -0,0 +1,208 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package refresh + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "time" + + configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" + "github.com/ratify-project/ratify/internal/constants" + cutils "github.com/ratify-project/ratify/pkg/controllers/utils" + kmp "github.com/ratify-project/ratify/pkg/keymanagementprovider" + "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type KubeRefresherNamespaced struct { + client.Client + Request ctrl.Request + Result ctrl.Result +} + +// Register registers the kubeRefresherNamespaced factory +func init() { + Register(KubeRefresherNamespacedType, &KubeRefresherNamespaced{}) +} + +// Refresh the certificates/keys for the key management provider by calling the GetCertificates and GetKeys methods +func (kr *KubeRefresherNamespaced) Refresh(ctx context.Context) error { + logger := logrus.WithContext(ctx) + + var resource = kr.Request.NamespacedName.String() + var keyManagementProvider configv1beta1.NamespacedKeyManagementProvider + + logger.Infof("reconciling namespaced key management provider '%v'", resource) + + if err := kr.Get(ctx, kr.Request.NamespacedName, &keyManagementProvider); err != nil { + if apierrors.IsNotFound(err) { + logger.Infof("deletion detected, removing key management provider %v", resource) + kmp.DeleteCertificatesFromMap(resource) + kmp.DeleteKeysFromMap(resource) + } else { + logger.Error(err, "unable to fetch key management provider") + } + + kr.Result = ctrl.Result{} + + return client.IgnoreNotFound(err) + } + + lastFetchedTime := metav1.Now() + isFetchSuccessful := false + + // get certificate store list to check if certificate store is configured + // TODO: remove check in v2.0.0+ + var certificateStoreList configv1beta1.CertificateStoreList + if err := kr.List(ctx, &certificateStoreList); err != nil { + logger.Error(err, "unable to list certificate stores") + kr.Result = ctrl.Result{} + return err + } + // if certificate store is configured, return error. Only one of certificate store and key management provider can be configured + if len(certificateStoreList.Items) > 0 { + // Note: for backwards compatibility in upgrade scenarios, Ratify will only log a warning statement. + logger.Warn("Certificate Store already exists. Key management provider and certificate store should not be configured together. Please migrate to key management provider and delete certificate store.") + } + + provider, err := cutils.SpecToKeyManagementProvider(keyManagementProvider.Spec.Parameters.Raw, keyManagementProvider.Spec.Type) + if err != nil { + writeKMProviderStatusNamespaced(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + kr.Result = ctrl.Result{} + return err + } + + // fetch certificates and store in map + certificates, certAttributes, err := provider.GetCertificates(ctx) + if err != nil { + writeKMProviderStatusNamespaced(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + kr.Result = ctrl.Result{} + return fmt.Errorf("error fetching certificates in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) + } + + // fetch keys and store in map + keys, keyAttributes, err := provider.GetKeys(ctx) + if err != nil { + writeKMProviderStatusNamespaced(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + kr.Result = ctrl.Result{} + return fmt.Errorf("error fetching keys in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) + } + kmp.SetCertificatesInMap(resource, certificates) + kmp.SetKeysInMap(resource, keyManagementProvider.Spec.Type, keys) + // merge certificates and keys status into one + maps.Copy(keyAttributes, certAttributes) + isFetchSuccessful = true + emptyErrorString := "" + writeKMProviderStatusNamespaced(ctx, kr, &keyManagementProvider, logger, isFetchSuccessful, emptyErrorString, lastFetchedTime, keyAttributes) + + logger.Infof("%v certificate(s) & %v key(s) fetched for key management provider %v", len(certificates), len(keys), resource) + + if !provider.IsRefreshable() { + kr.Result = ctrl.Result{} + return nil + } + + // if interval is not set, disable refresh + if keyManagementProvider.Spec.RefreshInterval == "" { + kr.Result = ctrl.Result{} + return nil + } + + intervalDuration, err := time.ParseDuration(keyManagementProvider.Spec.RefreshInterval) + if err != nil { + logger.Error(err, "unable to parse interval duration") + kr.Result = ctrl.Result{} + return err + } + + logger.Info("Reconciled KeyManagementProvider", "intervalDuration", intervalDuration) + kr.Result = ctrl.Result{RequeueAfter: intervalDuration} + + return nil +} + +// GetResult returns the result of the refresh as ctrl.Result +func (kr *KubeRefresherNamespaced) GetResult() interface{} { + return kr.Result +} + +// Create creates a new instance of KubeRefresherNamespaced +func (kr *KubeRefresherNamespaced) Create(config map[string]interface{}) (Refresher, error) { + client, ok := config["client"].(client.Client) + if !ok { + return nil, fmt.Errorf("client is required in config") + } + + request, ok := config["request"].(ctrl.Request) + if !ok { + return nil, fmt.Errorf("request is required in config") + } + + return &KubeRefresherNamespaced{ + Client: client, + Request: request, + }, nil +} + +// writeKMProviderStatus updates the status of the key management provider resource +func writeKMProviderStatusNamespaced(ctx context.Context, r client.StatusClient, keyManagementProvider *configv1beta1.NamespacedKeyManagementProvider, logger *logrus.Entry, isSuccess bool, errorString string, operationTime metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { + if isSuccess { + updateKMProviderSuccessStatusNamespaced(keyManagementProvider, &operationTime, kmProviderStatus) + } else { + updateKMProviderErrorStatusNamespaced(keyManagementProvider, errorString, &operationTime) + } + if statusErr := r.Status().Update(ctx, keyManagementProvider); statusErr != nil { + logger.Error(statusErr, ",unable to update key management provider error status") + } +} + +// updateKMProviderErrorStatus updates the key management provider status with error, brief error and last fetched time +func updateKMProviderErrorStatusNamespaced(keyManagementProvider *configv1beta1.NamespacedKeyManagementProvider, errorString string, operationTime *metav1.Time) { + // truncate brief error string to maxBriefErrLength + briefErr := errorString + if len(errorString) > constants.MaxBriefErrLength { + briefErr = fmt.Sprintf("%s...", errorString[:constants.MaxBriefErrLength]) + } + keyManagementProvider.Status.IsSuccess = false + keyManagementProvider.Status.Error = errorString + keyManagementProvider.Status.BriefError = briefErr + keyManagementProvider.Status.LastFetchedTime = operationTime +} + +// updateKMProviderSuccessStatus updates the key management provider status if status argument is non nil +// Success status includes last fetched time and other provider-specific properties +func updateKMProviderSuccessStatusNamespaced(keyManagementProvider *configv1beta1.NamespacedKeyManagementProvider, lastOperationTime *metav1.Time, kmProviderStatus kmp.KeyManagementProviderStatus) { + keyManagementProvider.Status.IsSuccess = true + keyManagementProvider.Status.Error = "" + keyManagementProvider.Status.BriefError = "" + keyManagementProvider.Status.LastFetchedTime = lastOperationTime + + if kmProviderStatus != nil { + jsonString, _ := json.Marshal(kmProviderStatus) + + raw := runtime.RawExtension{ + Raw: jsonString, + } + keyManagementProvider.Status.Properties = raw + } +} diff --git a/pkg/keymanagementprovider/refresh/kubeRefresh_test.go b/pkg/keymanagementprovider/refresh/kubeRefresh_test.go new file mode 100644 index 000000000..beb503753 --- /dev/null +++ b/pkg/keymanagementprovider/refresh/kubeRefresh_test.go @@ -0,0 +1,380 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package refresh + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + configv1beta1 "github.com/ratify-project/ratify/api/v1beta1" + "github.com/ratify-project/ratify/pkg/keymanagementprovider" + _ "github.com/ratify-project/ratify/pkg/keymanagementprovider/inline" + "github.com/ratify-project/ratify/pkg/keymanagementprovider/mocks" + test "github.com/ratify-project/ratify/pkg/utils" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestKubeRefresher_Refresh(t *testing.T) { + tests := []struct { + name string + provider *configv1beta1.KeyManagementProvider + request ctrl.Request + mockClient bool + expectedResult ctrl.Result + expectedError bool + }{ + { + name: "Non-refreshable", + provider: &configv1beta1.KeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.KeyManagementProviderSpec{ + Type: "inline", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"type": "inline", "contentType": "certificate", "value": "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{}, + expectedError: false, + }, + { + name: "Disabled", + provider: &configv1beta1.KeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.KeyManagementProviderSpec{ + Type: "test-kmp", + RefreshInterval: "", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{}, + expectedError: false, + }, + { + name: "Refreshable", + provider: &configv1beta1.KeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.KeyManagementProviderSpec{ + Type: "test-kmp", + RefreshInterval: "1m", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedError: false, + }, + { + name: "Invalid Interval", + provider: &configv1beta1.KeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.KeyManagementProviderSpec{ + Type: "", + RefreshInterval: "1mm", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + request: ctrl.Request{ + NamespacedName: client.ObjectKey{ + Namespace: "", + Name: "kmpName", + }, + }, + expectedResult: ctrl.Result{}, + expectedError: true, + }, + { + name: "IsNotFound", + provider: &configv1beta1.KeyManagementProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "kmpName", + }, + Spec: configv1beta1.KeyManagementProviderSpec{ + Type: "", + RefreshInterval: "", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + }, + }, + }, + expectedResult: ctrl.Result{}, + expectedError: false, + }, + { + name: "UnableToFetchKMP", + mockClient: true, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var client client.Client + + if tt.mockClient { + client = mocks.TestClient{} + } else { + scheme, _ := test.CreateScheme() + client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.provider).Build() + } + + kr := &KubeRefresher{ + Client: client, + Request: tt.request, + } + err := kr.Refresh(context.Background()) + result := kr.GetResult() + if !reflect.DeepEqual(result, tt.expectedResult) { + t.Fatalf("Expected nil but got %v with error %v", result, err) + } + if tt.expectedError && err == nil { + t.Fatalf("Expected error but got nil") + } + }) + } +} + +func TestKubeRefresher_Create(t *testing.T) { + tests := []struct { + name string + config map[string]interface{} + expectedError bool + }{ + { + name: "Success", + config: map[string]interface{}{ + "client": &mocks.TestClient{}, + "request": ctrl.Request{}, + }, + expectedError: false, + }, + { + name: "ClientMissing", + config: map[string]interface{}{ + "request": ctrl.Request{}, + }, + expectedError: true, + }, + { + name: "RequestMissing", + config: map[string]interface{}{ + "client": &mocks.TestClient{}, + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kr := &KubeRefresher{} + _, err := kr.Create(tt.config) + if tt.expectedError && err == nil { + t.Fatalf("Expected error but got nil") + } + }) + } +} + +func TestKMProviderUpdateErrorStatus(t *testing.T) { + var parametersString = "{\"certs\":{\"name\":\"certName\"}}" + var kmProviderStatus = []byte(parametersString) + + status := configv1beta1.KeyManagementProviderStatus{ + IsSuccess: true, + Properties: runtime.RawExtension{ + Raw: kmProviderStatus, + }, + } + keyManagementProvider := configv1beta1.KeyManagementProvider{ + Status: status, + } + expectedErr := "it's a long error from unit test" + lastFetchedTime := metav1.Now() + updateKMProviderErrorStatus(&keyManagementProvider, expectedErr, &lastFetchedTime) + + if keyManagementProvider.Status.IsSuccess != false { + t.Fatalf("Unexpected error, expected isSuccess to be false , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != expectedErr { + t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedErr, keyManagementProvider.Status.Error) + } + expectedBriedErr := fmt.Sprintf("%s...", expectedErr[:30]) + if keyManagementProvider.Status.BriefError != expectedBriedErr { + t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedBriedErr, keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was not overridden + if len(keyManagementProvider.Status.Properties.Raw) == 0 { + t.Fatalf("Unexpected properties, expected %+v, got %+v", parametersString, string(keyManagementProvider.Status.Properties.Raw)) + } +} + +// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method +func TestKMProviderUpdateSuccessStatus(t *testing.T) { + kmProviderStatus := keymanagementprovider.KeyManagementProviderStatus{} + properties := map[string]string{} + properties["Name"] = "wabbit" + properties["Version"] = "ABC" + + kmProviderStatus["Certificates"] = properties + + lastFetchedTime := metav1.Now() + + status := configv1beta1.KeyManagementProviderStatus{ + IsSuccess: false, + Error: "error from last operation", + } + keyManagementProvider := configv1beta1.KeyManagementProvider{ + Status: status, + } + + updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, kmProviderStatus) + + if keyManagementProvider.Status.IsSuccess != true { + t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != "" { + t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was updated + if len(keyManagementProvider.Status.Properties.Raw) == 0 { + t.Fatalf("Properties should not be empty") + } +} + +// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method with empty properties +func TestKMProviderUpdateSuccessStatus_emptyProperties(t *testing.T) { + lastFetchedTime := metav1.Now() + status := configv1beta1.KeyManagementProviderStatus{ + IsSuccess: false, + Error: "error from last operation", + } + keyManagementProvider := configv1beta1.KeyManagementProvider{ + Status: status, + } + + updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, nil) + + if keyManagementProvider.Status.IsSuccess != true { + t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != "" { + t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was updated + if len(keyManagementProvider.Status.Properties.Raw) != 0 { + t.Fatalf("Properties should be empty") + } +} + +func TestWriteKMProviderStatus(t *testing.T) { + logger := logrus.WithContext(context.Background()) + lastFetchedTime := metav1.Now() + testCases := []struct { + name string + isSuccess bool + kmProvider *configv1beta1.KeyManagementProvider + errString string + reconciler client.StatusClient + }{ + { + name: "success status", + isSuccess: true, + errString: "", + kmProvider: &configv1beta1.KeyManagementProvider{}, + reconciler: &test.MockStatusClient{}, + }, + { + name: "error status", + isSuccess: false, + kmProvider: &configv1beta1.KeyManagementProvider{}, + errString: "a long error string that exceeds the max length of 30 characters", + reconciler: &test.MockStatusClient{}, + }, + { + name: "status update failed", + isSuccess: true, + kmProvider: &configv1beta1.KeyManagementProvider{}, + reconciler: &test.MockStatusClient{ + UpdateFailed: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + writeKMProviderStatus(context.Background(), tc.reconciler, tc.kmProvider, logger, tc.isSuccess, tc.errString, lastFetchedTime, nil) + + if tc.kmProvider.Status.IsSuccess != tc.isSuccess { + t.Fatalf("Expected isSuccess to be %+v , actual %+v", tc.isSuccess, tc.kmProvider.Status.IsSuccess) + } + + if tc.kmProvider.Status.Error != tc.errString { + t.Fatalf("Expected Error to be %+v , actual %+v", tc.errString, tc.kmProvider.Status.Error) + } + }) + } +} diff --git a/pkg/keymanagementprovider/refresh/refresh.go b/pkg/keymanagementprovider/refresh/refresh.go new file mode 100644 index 000000000..78e32b12d --- /dev/null +++ b/pkg/keymanagementprovider/refresh/refresh.go @@ -0,0 +1,34 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package refresh + +import ( + "context" +) + +const ( + KubeRefresherType = "kubeRefresher" + KubeRefresherNamespacedType = "kubeRefresherNamespaced" +) + +// Refresher is an interface that defines methods to be implemented by a each refresher +type Refresher interface { + // Refresh is a method that refreshes the certificates/keys + Refresh(ctx context.Context) error + // GetResult is a method that returns the result of the refresh + GetResult() interface{} +} diff --git a/pkg/keymanagementprovider/refresh/test_helper_test.go b/pkg/keymanagementprovider/refresh/test_helper_test.go new file mode 100644 index 000000000..0fd82a12a --- /dev/null +++ b/pkg/keymanagementprovider/refresh/test_helper_test.go @@ -0,0 +1,26 @@ +/* +Copyright The Ratify Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package refresh + +import ( + "github.com/ratify-project/ratify/pkg/keymanagementprovider/factory" + "github.com/ratify-project/ratify/pkg/keymanagementprovider/mocks" +) + +func init() { + // Register the mock KeyManagementProviderFactory + factory.Register("test-kmp", &mocks.TestKeyManagementProviderFactory{}) +}