From 7abfc7fc8a3f1b0b12d5cefb19bad24b53662137 Mon Sep 17 00:00:00 2001 From: Susan Shi Date: Tue, 30 Jan 2024 15:25:02 -0800 Subject: [PATCH] feat: validate plugin name on CR create (#1265) Signed-off-by: Susan Shi --- .vscode/launch.json | 3 +- Makefile | 1 + api/v1beta1/store_types.go | 14 ++- api/v1beta1/verifier_types.go | 14 ++- .../crds/store-customresourcedefinition.yaml | 27 +++++- .../verifier-customresourcedefinition.yaml | 25 +++++- .../config.ratify.deislabs.io_stores.yaml | 25 +++++- .../config.ratify.deislabs.io_verifiers.yaml | 25 +++++- .../samples/config_v1beta1_store_dynamic.yaml | 8 ++ pkg/controllers/policy_controller.go | 4 +- pkg/controllers/store_controller.go | 21 +++++ pkg/controllers/store_controller_test.go | 81 +++++++++++++++++- pkg/controllers/verifier_controller.go | 21 +++++ pkg/controllers/verifier_controller_test.go | 85 ++++++++++++++++++- pkg/referrerstore/factory/factory.go | 6 ++ pkg/referrerstore/factory/factory_test.go | 19 +++-- pkg/referrerstore/plugin/skel/skel_test.go | 14 ++- pkg/verifier/factory/factory.go | 6 ++ pkg/verifier/factory/factory_test.go | 9 +- pkg/verifier/plugin/skel/skel_test.go | 28 +++--- test/bats/base-test.bats | 44 ++++++++++ 21 files changed, 440 insertions(+), 40 deletions(-) create mode 100644 config/samples/config_v1beta1_store_dynamic.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json index 74a94a21f..04f87f571 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -46,7 +46,8 @@ "program": "${workspaceFolder}/cmd/ratify", "env": { "RATIFY_LOG_LEVEL": "debug", - "RATIFY_EXPERIMENTAL_DYNAMIC_PLUGINS": "1" + "RATIFY_EXPERIMENTAL_DYNAMIC_PLUGINS": "1", + "RATIFY_NAMESPACE": "gatekeeper-system", }, "args": [ "serve", diff --git a/Makefile b/Makefile index 3362a14e2..a217d7e83 100644 --- a/Makefile +++ b/Makefile @@ -82,6 +82,7 @@ build-plugins: go build -cover -coverpkg=github.com/deislabs/ratify/plugins/verifier/cosign/... -o ./bin/plugins/ ./plugins/verifier/cosign go build -cover -coverpkg=github.com/deislabs/ratify/plugins/verifier/licensechecker/... -o ./bin/plugins/ ./plugins/verifier/licensechecker go build -cover -coverpkg=github.com/deislabs/ratify/plugins/verifier/sample/... -o ./bin/plugins/ ./plugins/verifier/sample + go build -cover -coverpkg=github.com/deislabs/ratify/plugins/referrerstore/sample/... -o ./bin/plugins/referrerstore/ ./plugins/referrerstore/sample go build -cover -coverpkg=github.com/deislabs/ratify/plugins/verifier/sbom/... -o ./bin/plugins/ ./plugins/verifier/sbom go build -cover -coverpkg=github.com/deislabs/ratify/plugins/verifier/schemavalidator/... -o ./bin/plugins/ ./plugins/verifier/schemavalidator go build -cover -coverpkg=github.com/deislabs/ratify/plugins/verifier/vulnerabilityreport/... -o ./bin/plugins/ ./plugins/verifier/vulnerabilityreport diff --git a/api/v1beta1/store_types.go b/api/v1beta1/store_types.go index d47eadf11..344227870 100644 --- a/api/v1beta1/store_types.go +++ b/api/v1beta1/store_types.go @@ -41,12 +41,24 @@ type StoreSpec struct { // StoreStatus defines the observed state of Store type StoreStatus struct { - // Important: Run "make" to regenerate code after modifying this file + // Important: Run "make install-crds" to regenerate code after modifying this file + + // Is successful in finding the plugin + IsSuccess bool `json:"issuccess"` + // Error message if operation was unsuccessful + // +optional + Error string `json:"error,omitempty"` + // Truncated error message if the message is too long + // +optional + BriefError string `json:"brieferror,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:resource:scope="Cluster" +// +kubebuilder:subresource:status // +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="IsSuccess",type=boolean,JSONPath=`.status.issuccess` +// +kubebuilder:printcolumn:name="Error",type=string,JSONPath=`.status.brieferror` // Store is the Schema for the stores API type Store struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1beta1/verifier_types.go b/api/v1beta1/verifier_types.go index 6f3c6e141..5d4bf0974 100644 --- a/api/v1beta1/verifier_types.go +++ b/api/v1beta1/verifier_types.go @@ -48,12 +48,24 @@ type VerifierSpec struct { // VerifierStatus defines the observed state of Verifier type VerifierStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + // Important: Run "make install-crds" to regenerate code after modifying this file + + // Is successful in finding the plugin + IsSuccess bool `json:"issuccess"` + // Error message if operation was unsuccessful + // +optional + Error string `json:"error,omitempty"` + // Truncated error message if the message is too long + // +optional + BriefError string `json:"brieferror,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:resource:scope="Cluster" +// +kubebuilder:subresource:status // +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="IsSuccess",type=boolean,JSONPath=`.status.issuccess` +// +kubebuilder:printcolumn:name="Error",type=string,JSONPath=`.status.brieferror` // Verifier is the Schema for the verifiers API type Verifier struct { metav1.TypeMeta `json:",inline"` diff --git a/charts/ratify/crds/store-customresourcedefinition.yaml b/charts/ratify/crds/store-customresourcedefinition.yaml index 8827fa0b0..46aa5a8be 100644 --- a/charts/ratify/crds/store-customresourcedefinition.yaml +++ b/charts/ratify/crds/store-customresourcedefinition.yaml @@ -66,7 +66,14 @@ spec: type: object served: true storage: false - - name: v1beta1 + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + name: v1beta1 schema: openAPIV3Schema: description: Store is the Schema for the stores API @@ -94,7 +101,7 @@ spec: type: string version: description: Version of the store plugin. Optional - type: string + type: string parameters: description: Parameters of the store type: object @@ -110,13 +117,27 @@ spec: source, optional type: object x-kubernetes-preserve-unknown-fields: true - type: object + type: object required: - name type: object status: description: StoreStatus defines the observed state of Store + 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 finding the plugin + type: boolean + required: + - issuccess type: object type: object served: true storage: true + subresources: + status: {} diff --git a/charts/ratify/crds/verifier-customresourcedefinition.yaml b/charts/ratify/crds/verifier-customresourcedefinition.yaml index ccaaddc0e..0d242aef8 100644 --- a/charts/ratify/crds/verifier-customresourcedefinition.yaml +++ b/charts/ratify/crds/verifier-customresourcedefinition.yaml @@ -69,7 +69,14 @@ spec: type: object served: true storage: false - - name: v1beta1 + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + name: v1beta1 schema: openAPIV3Schema: description: Verifier is the Schema for the verifiers API @@ -100,7 +107,7 @@ spec: type: string version: description: Version of the verifier plugin. Optional - type: string + type: string parameters: description: Parameters for this verifier type: object @@ -123,7 +130,21 @@ spec: type: object status: description: VerifierStatus defines the observed state of Verifier + 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 finding the plugin + type: boolean + required: + - issuccess type: object type: object served: true storage: true + subresources: + status: {} diff --git a/config/crd/bases/config.ratify.deislabs.io_stores.yaml b/config/crd/bases/config.ratify.deislabs.io_stores.yaml index 409dc7d19..0f34fe68d 100644 --- a/config/crd/bases/config.ratify.deislabs.io_stores.yaml +++ b/config/crd/bases/config.ratify.deislabs.io_stores.yaml @@ -67,7 +67,14 @@ spec: type: object served: true storage: false - - name: v1beta1 + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + name: v1beta1 schema: openAPIV3Schema: description: Store is the Schema for the stores API @@ -95,7 +102,7 @@ spec: type: string version: description: Version of the store plugin. Optional - type: string + type: string parameters: description: Parameters of the store type: object @@ -117,7 +124,21 @@ spec: type: object status: description: StoreStatus defines the observed state of Store + 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 finding the plugin + type: boolean + required: + - issuccess type: object type: object served: true storage: true + subresources: + status: {} diff --git a/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml b/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml index 36584a928..879e6fd7f 100644 --- a/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml +++ b/config/crd/bases/config.ratify.deislabs.io_verifiers.yaml @@ -70,7 +70,14 @@ spec: type: object served: true storage: false - - name: v1beta1 + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + name: v1beta1 schema: openAPIV3Schema: description: Verifier is the Schema for the verifiers API @@ -101,7 +108,7 @@ spec: type: string version: description: Version of the verifier plugin. Optional - type: string + type: string parameters: description: Parameters for this verifier type: object @@ -124,7 +131,21 @@ spec: type: object status: description: VerifierStatus defines the observed state of Verifier + 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 finding the plugin + type: boolean + required: + - issuccess type: object type: object served: true storage: true + subresources: + status: {} diff --git a/config/samples/config_v1beta1_store_dynamic.yaml b/config/samples/config_v1beta1_store_dynamic.yaml new file mode 100644 index 000000000..f916ce8e2 --- /dev/null +++ b/config/samples/config_v1beta1_store_dynamic.yaml @@ -0,0 +1,8 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: Store +metadata: + name: store-dynamic +spec: + name: dynamic + source: + artifact: wabbitnetworks.azurecr.io/test/sample-store-plugin:v1 diff --git a/pkg/controllers/policy_controller.go b/pkg/controllers/policy_controller.go index 109071c40..5f29a55e1 100644 --- a/pkg/controllers/policy_controller.go +++ b/pkg/controllers/policy_controller.go @@ -88,6 +88,7 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } + writePolicyStatus(ctx, r, &policy, policyLogger, true, "") return ctrl.Result{}, nil } @@ -159,13 +160,14 @@ func writePolicyStatus(ctx context.Context, r client.StatusClient, policy *confi updatePolicyErrorStatus(policy, errString) } if statusErr := r.Status().Update(ctx, policy); statusErr != nil { - logger.Error(statusErr, ", unbale to update policy error status") + logger.Error(statusErr, ", unable to update policy error status") } } func updatePolicySuccessStatus(policy *configv1beta1.Policy) { policy.Status.IsSuccess = true policy.Status.Error = "" + policy.Status.BriefError = "" } func updatePolicyErrorStatus(policy *configv1beta1.Policy, errString string) { diff --git a/pkg/controllers/store_controller.go b/pkg/controllers/store_controller.go index 5f0b1e13e..b4421a6ef 100644 --- a/pkg/controllers/store_controller.go +++ b/pkg/controllers/store_controller.go @@ -74,9 +74,12 @@ func (r *StoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl if err := storeAddOrReplace(store.Spec, resource); err != nil { storeLogger.Error(err, "unable to create store from store crd") + writeStoreStatus(ctx, r, &store, storeLogger, false, err.Error()) return ctrl.Result{}, err } + writeStoreStatus(ctx, r, &store, storeLogger, true, "") + // returning empty result and no error to indicate we’ve successfully reconciled this object return ctrl.Result{}, nil } @@ -140,3 +143,21 @@ func specToStoreConfig(storeSpec configv1beta1.StoreSpec) (rc.StorePluginConfig, return storeConfig, nil } + +func writeStoreStatus(ctx context.Context, r client.StatusClient, store *configv1beta1.Store, logger *logrus.Entry, isSuccess bool, errorString string) { + if isSuccess { + store.Status.IsSuccess = true + store.Status.Error = "" + store.Status.BriefError = "" + } else { + store.Status.IsSuccess = false + store.Status.Error = errorString + if len(errorString) > maxBriefErrLength { + store.Status.BriefError = fmt.Sprintf("%s...", errorString[:maxBriefErrLength]) + } + } + + if statusErr := r.Status().Update(ctx, store); statusErr != nil { + logger.Error(statusErr, ",unable to update store error status") + } +} diff --git a/pkg/controllers/store_controller_test.go b/pkg/controllers/store_controller_test.go index 167744bd7..75196d856 100644 --- a/pkg/controllers/store_controller_test.go +++ b/pkg/controllers/store_controller_test.go @@ -16,17 +16,24 @@ limitations under the License. package controllers import ( + "context" + "os" + "path/filepath" + "strings" "testing" configv1beta1 "github.com/deislabs/ratify/api/v1beta1" "github.com/deislabs/ratify/pkg/referrerstore" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) func TestStoreAdd_EmptyParameter(t *testing.T) { resetStoreMap() var testStoreSpec = configv1beta1.StoreSpec{ - Name: "oras", + Name: "sample", + Address: getVerifierPluginsDir(), } if err := storeAddOrReplace(testStoreSpec, "oras"); err != nil { @@ -53,11 +60,62 @@ func TestStoreAdd_WithParameters(t *testing.T) { } } +func TestWriteStoreStatus(t *testing.T) { + logger := logrus.WithContext(context.Background()) + testCases := []struct { + name string + isSuccess bool + store *configv1beta1.Store + errString string + reconciler client.StatusClient + }{ + { + name: "success status", + isSuccess: true, + store: &configv1beta1.Store{}, + reconciler: &mockStatusClient{}, + }, + { + name: "error status", + isSuccess: false, + store: &configv1beta1.Store{}, + errString: "a long error string that exceeds the max length of 30 characters", + reconciler: &mockStatusClient{}, + }, + { + name: "status update failed", + isSuccess: true, + store: &configv1beta1.Store{}, + reconciler: &mockStatusClient{ + updateFailed: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + writeStoreStatus(context.Background(), tc.reconciler, tc.store, logger, tc.isSuccess, tc.errString) + }) + } +} + +func TestStoreAddOrReplace_PluginNotFound(t *testing.T) { + resetStoreMap() + var resource = "invalidplugin" + expectedMsg := "plugin not found" + var spec = getInvalidStoreSpec() + err := storeAddOrReplace(spec, resource) + + if !strings.Contains(err.Error(), expectedMsg) { + t.Fatalf("TestStoreAddOrReplace_PluginNotFound expected msg: '%v', actual %v", expectedMsg, err.Error()) + } +} + func TestStore_UpdateAndDelete(t *testing.T) { resetStoreMap() // add a Store - var resource = "oras" + var resource = "sample" var testStoreSpec = getOrasStoreSpec() @@ -70,7 +128,8 @@ func TestStore_UpdateAndDelete(t *testing.T) { // modify the Store var updatedSpec = configv1beta1.StoreSpec{ - Name: "oras", + Name: "sample", + Address: getStorePluginsDir(), } if err := storeAddOrReplace(updatedSpec, resource); err != nil { @@ -98,9 +157,23 @@ func getOrasStoreSpec() configv1beta1.StoreSpec { var storeParameters = []byte(parametersString) return configv1beta1.StoreSpec{ - Name: "oras", + Name: "sample", + Address: getStorePluginsDir(), Parameters: runtime.RawExtension{ Raw: storeParameters, }, } } + +func getStorePluginsDir() string { + workingDir, _ := os.Getwd() + pluginDir := filepath.Clean(filepath.Join(workingDir, "../..", "./bin/plugins/referrerstore/")) + return pluginDir +} + +func getInvalidStoreSpec() configv1beta1.StoreSpec { + return configv1beta1.StoreSpec{ + Name: "pluginnotfound", + Address: getStorePluginsDir(), + } +} diff --git a/pkg/controllers/verifier_controller.go b/pkg/controllers/verifier_controller.go index ee59fb6e0..31cb83a5b 100644 --- a/pkg/controllers/verifier_controller.go +++ b/pkg/controllers/verifier_controller.go @@ -88,9 +88,12 @@ func (r *VerifierReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if err = verifierAddOrReplace(verifier.Spec, resource, namespace); err != nil { verifierLogger.Error(err, "unable to create verifier from verifier crd") + writeVerifierStatus(ctx, r, &verifier, verifierLogger, false, err.Error()) return ctrl.Result{}, err } + writeVerifierStatus(ctx, r, &verifier, verifierLogger, true, "") + // returning empty result and no error to indicate we’ve successfully reconciled this object return ctrl.Result{}, nil } @@ -173,3 +176,21 @@ func getCertStoreNamespace(verifierNamespace string) (string, error) { return ns, nil } + +func writeVerifierStatus(ctx context.Context, r client.StatusClient, verifier *configv1beta1.Verifier, logger *logrus.Entry, isSuccess bool, errorString string) { + if isSuccess { + verifier.Status.IsSuccess = true + verifier.Status.Error = "" + verifier.Status.BriefError = "" + } else { + verifier.Status.IsSuccess = false + verifier.Status.Error = errorString + if len(errorString) > maxBriefErrLength { + verifier.Status.BriefError = fmt.Sprintf("%s...", errorString[:maxBriefErrLength]) + } + } + + if statusErr := r.Status().Update(ctx, verifier); statusErr != nil { + logger.Error(statusErr, ",unable to update verifier status") + } +} diff --git a/pkg/controllers/verifier_controller_test.go b/pkg/controllers/verifier_controller_test.go index ca4b7f1ed..1ab59011d 100644 --- a/pkg/controllers/verifier_controller_test.go +++ b/pkg/controllers/verifier_controller_test.go @@ -16,14 +16,19 @@ limitations under the License. package controllers import ( + "context" "os" + "path/filepath" + "strings" "testing" configv1beta1 "github.com/deislabs/ratify/api/v1beta1" "github.com/deislabs/ratify/internal/constants" "github.com/deislabs/ratify/pkg/utils" vr "github.com/deislabs/ratify/pkg/verifier" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) func TestMain(m *testing.M) { @@ -36,10 +41,11 @@ func TestMain(m *testing.M) { func TestVerifierAdd_EmptyParameter(t *testing.T) { resetVerifierMap() var testVerifierSpec = configv1beta1.VerifierSpec{ - Name: "notation", + Name: "sample", ArtifactTypes: "application/vnd.cncf.notary.signature", + Address: getVerifierPluginsDir(), } - var resource = "notation" + var resource = "sample" if err := verifierAddOrReplace(testVerifierSpec, resource, constants.EmptyNamespace); err != nil { t.Fatalf("verifierAddOrReplace() expected no error, actual %v", err) @@ -65,6 +71,18 @@ func TestVerifierAdd_WithParameters(t *testing.T) { } } +func TestVerifierAddOrReplace_PluginNotFound(t *testing.T) { + resetVerifierMap() + var resource = "invalidplugin" + expectedMsg := "plugin not found" + var testVerifierSpec = getInvalidVerifierSpec() + err := verifierAddOrReplace(testVerifierSpec, resource, constants.EmptyNamespace) + + if !strings.Contains(err.Error(), expectedMsg) { + t.Fatalf("TestVerifierAddOrReplace_PluginNotFound expected msg: '%v', actual %v", expectedMsg, err.Error()) + } +} + func TestVerifier_UpdateAndDelete(t *testing.T) { resetVerifierMap() @@ -98,6 +116,54 @@ func TestVerifier_UpdateAndDelete(t *testing.T) { } } +func TestWriteVerifierStatus(t *testing.T) { + logger := logrus.WithContext(context.Background()) + testCases := []struct { + name string + isSuccess bool + verifier *configv1beta1.Verifier + errString string + reconciler client.StatusClient + }{ + { + name: "success status", + isSuccess: true, + errString: "", + verifier: &configv1beta1.Verifier{}, + reconciler: &mockStatusClient{}, + }, + { + name: "error status", + isSuccess: false, + verifier: &configv1beta1.Verifier{}, + errString: "a long error string that exceeds the max length of 30 characters", + reconciler: &mockStatusClient{}, + }, + { + name: "status update failed", + isSuccess: true, + verifier: &configv1beta1.Verifier{}, + reconciler: &mockStatusClient{ + updateFailed: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + writeVerifierStatus(context.Background(), tc.reconciler, tc.verifier, logger, tc.isSuccess, tc.errString) + + if tc.verifier.Status.IsSuccess != tc.isSuccess { + t.Fatalf("Expected isSuccess to be %+v , actual %+v", tc.isSuccess, tc.verifier.Status.IsSuccess) + } + + if tc.verifier.Status.Error != tc.errString { + t.Fatalf("Expected Error to be %+v , actual %+v", tc.errString, tc.verifier.Status.Error) + } + }) + } +} + func TestGetCertStoreNamespace(t *testing.T) { // error scenario, everything is empty, expect error _, err := getCertStoreNamespace("") @@ -133,13 +199,28 @@ func getLicenseCheckerFromParam(parametersString string) configv1beta1.VerifierS return configv1beta1.VerifierSpec{ Name: "licensechecker", ArtifactTypes: "application/vnd.ratify.spdx.v0", + Address: getVerifierPluginsDir(), Parameters: runtime.RawExtension{ Raw: allowedLicenses, }, } } +func getInvalidVerifierSpec() configv1beta1.VerifierSpec { + return configv1beta1.VerifierSpec{ + Name: "pluginnotfound", + ArtifactTypes: "application/vnd.ratify.spdx.v0", + Address: getVerifierPluginsDir(), + } +} + func getDefaultLicenseCheckerSpec() configv1beta1.VerifierSpec { var parametersString = "{\"allowedLicenses\":[\"MIT\",\"Apache\"]}" return getLicenseCheckerFromParam(parametersString) } + +func getVerifierPluginsDir() string { + workingDir, _ := os.Getwd() + pluginDir := filepath.Clean(filepath.Join(workingDir, "../..", "./bin/plugins")) + return pluginDir +} diff --git a/pkg/referrerstore/factory/factory.go b/pkg/referrerstore/factory/factory.go index 099632abf..fb80faa50 100644 --- a/pkg/referrerstore/factory/factory.go +++ b/pkg/referrerstore/factory/factory.go @@ -50,6 +50,7 @@ func Register(name string, factory StoreFactory) { builtInStores[name] = factory } +// the first element of pluginBinDir will be used as the plugin directory func CreateStoreFromConfig(storeConfig config.StorePluginConfig, configVersion string, pluginBinDir []string) (referrerstore.ReferrerStore, error) { storeName, ok := storeConfig[types.Name] if !ok { @@ -84,6 +85,11 @@ func CreateStoreFromConfig(storeConfig config.StorePluginConfig, configVersion s if ok { return storeFactory.Create(configVersion, storeConfig) } + + if _, err := pluginCommon.FindInPaths(storeNameStr, pluginBinDir); err != nil { + return nil, re.ErrorCodePluginNotFound.NewError(re.ReferrerStore, "", re.EmptyLink, err, "plugin not found", re.HideStackTrace) + } + return plugin.NewStore(configVersion, storeConfig, pluginBinDir) } diff --git a/pkg/referrerstore/factory/factory_test.go b/pkg/referrerstore/factory/factory_test.go index 9e46d32b8..babd4b916 100644 --- a/pkg/referrerstore/factory/factory_test.go +++ b/pkg/referrerstore/factory/factory_test.go @@ -19,6 +19,7 @@ import ( "errors" "os" "path" + "path/filepath" "testing" "github.com/deislabs/ratify/pkg/featureflag" @@ -46,7 +47,7 @@ func TestCreateStoresFromConfig_BuiltInStores_ReturnsExpected(t *testing.T) { Stores: []config.StorePluginConfig{storeConfig}, } - stores, err := CreateStoresFromConfig(storesConfig, "") + stores, err := CreateStoresFromConfig(storesConfig, getReferrerstorePluginsDir()) if err != nil { t.Fatalf("create stores failed with err %v", err) @@ -67,13 +68,13 @@ func TestCreateStoresFromConfig_BuiltInStores_ReturnsExpected(t *testing.T) { func TestCreateStoresFromConfig_PluginStores_ReturnsExpected(t *testing.T) { storeConfig := map[string]interface{}{ - "name": "plugin-store", + "name": "sample", } storesConfig := config.StoresConfig{ Stores: []config.StorePluginConfig{storeConfig}, } - stores, err := CreateStoresFromConfig(storesConfig, "") + stores, err := CreateStoresFromConfig(storesConfig, getReferrerstorePluginsDir()) if err != nil { t.Fatalf("create stores failed with err %v", err) @@ -83,7 +84,7 @@ func TestCreateStoresFromConfig_PluginStores_ReturnsExpected(t *testing.T) { t.Fatalf("expected to have %d stores, actual count %d", 1, len(stores)) } - if stores[0].Name() != "plugin-store" { + if stores[0].Name() != "sample" { t.Fatalf("expected to create plugin store") } @@ -95,6 +96,7 @@ func TestCreateStoresFromConfig_PluginStores_ReturnsExpected(t *testing.T) { func TestCreateStoresFromConfig_DynamicPluginStores_ReturnsExpected(t *testing.T) { os.Setenv("RATIFY_EXPERIMENTAL_DYNAMIC_PLUGINS", "1") featureflag.InitFeatureFlagsFromEnv() + testCases := []struct { name string artifact string @@ -121,8 +123,7 @@ func TestCreateStoresFromConfig_DynamicPluginStores_ReturnsExpected(t *testing.T storesConfig := config.StoresConfig{ Stores: []config.StorePluginConfig{storeConfig}, } - - stores, err := CreateStoresFromConfig(storesConfig, "") + stores, err := CreateStoresFromConfig(storesConfig, getReferrerstorePluginsDir()) if err != nil { t.Fatalf("create stores failed with err %v", err) @@ -147,3 +148,9 @@ func TestCreateStoresFromConfig_DynamicPluginStores_ReturnsExpected(t *testing.T }) } } + +func getReferrerstorePluginsDir() string { + workingDir, _ := os.Getwd() + pluginDir := filepath.Clean(filepath.Join(workingDir, "../../../", "./bin/plugins/referrerstore/")) + return pluginDir +} diff --git a/pkg/referrerstore/plugin/skel/skel_test.go b/pkg/referrerstore/plugin/skel/skel_test.go index 1f58993c3..9022637fb 100644 --- a/pkg/referrerstore/plugin/skel/skel_test.go +++ b/pkg/referrerstore/plugin/skel/skel_test.go @@ -18,6 +18,8 @@ package skel import ( "bytes" "fmt" + "os" + "path/filepath" "strings" "testing" @@ -30,8 +32,8 @@ import ( v1 "github.com/opencontainers/image-spec/specs-go/v1" ) -const ( - testStdinData = `{ "name":"skel-test-case", "some": "config" }` +var ( + testStdinData = fmt.Sprintf(`{ "name":"skel-test-case", "some": "config","pluginBinDirs": ["%s"]}`, getPluginsDir()) ) func TestPluginMain_GetBlobContent_ReturnsExpected(t *testing.T) { @@ -220,7 +222,7 @@ func TestPluginMain_ErrorCases(t *testing.T) { pluginContext.Stdin = strings.NewReader(stdinData) err = pluginContext.pluginMainCore("skel-test-case", "1.0.0", nil, getBlobContent, nil, nil, []string{"1.0.0"}) if err == nil || err.Code != types.ErrConfigParsingFailure { - t.Fatalf("plugin execution expected to fail with error code %d for invalid config", types.ErrConfigParsingFailure) + t.Fatalf("plugin execution expected to fail with error code %d for invalid config, actual error: %s", types.ErrConfigParsingFailure, err) } stdinData = ` {"some": "config" }` @@ -316,3 +318,9 @@ func TestPluginMain_ListReferrers_ErrorCases(t *testing.T) { t.Fatalf("plugin execution expected to fail with error code %d for invalid arg", types.ErrArgsParsingFailure) } } + +func getPluginsDir() string { + workingDir, _ := os.Getwd() + pluginDir := filepath.Clean(filepath.Join(workingDir, "../../../../", "./bin/plugins/referrerstore/")) + return pluginDir +} diff --git a/pkg/verifier/factory/factory.go b/pkg/verifier/factory/factory.go index 19e1403cc..43e16be3c 100644 --- a/pkg/verifier/factory/factory.go +++ b/pkg/verifier/factory/factory.go @@ -51,6 +51,7 @@ func Register(name string, factory VerifierFactory) { // returns a single verifier from a verifierConfig // namespace is only applicable in K8s environment, namespace is appended to the certstore of the truststore so it is uniquely identifiable in a cluster env +// the first element of pluginBinDir will be used as the plugin directory func CreateVerifierFromConfig(verifierConfig config.VerifierConfig, configVersion string, pluginBinDir []string, namespace string) (verifier.ReferenceVerifier, error) { // in cli mode both `type` and `name`` are read from config, if `type` is not specified, `name` is used as `type` var verifierTypeStr string @@ -91,6 +92,11 @@ func CreateVerifierFromConfig(verifierConfig config.VerifierConfig, configVersio if ok { return verifierFactory.Create(configVersion, verifierConfig, pluginBinDir[0], namespace) } + + if _, err := pluginCommon.FindInPaths(verifierTypeStr, pluginBinDir); err != nil { + return nil, re.ErrorCodePluginNotFound.NewError(re.Verifier, "", re.EmptyLink, err, "plugin not found", re.HideStackTrace) + } + return plugin.NewVerifier(configVersion, verifierConfig, pluginBinDir) } diff --git a/pkg/verifier/factory/factory_test.go b/pkg/verifier/factory/factory_test.go index 9632f3e77..54a865bce 100644 --- a/pkg/verifier/factory/factory_test.go +++ b/pkg/verifier/factory/factory_test.go @@ -17,6 +17,8 @@ package factory import ( "context" + "os" + "path/filepath" "testing" "github.com/deislabs/ratify/internal/constants" @@ -102,15 +104,18 @@ func TestCreateVerifiersFromConfig_BuiltInVerifiers_ReturnsExpected(t *testing.T } func TestCreateVerifiersFromConfig_PluginVerifiers_ReturnsExpected(t *testing.T) { + workingDir, _ := os.Getwd() + pluginDir := filepath.Clean(filepath.Join(workingDir, "../../..", "./bin/plugins")) + verifierConfig := map[string]interface{}{ "name": "plugin-verifier-0", - "type": "plugin-verifier", + "type": "sample", } verifiersConfig := config.VerifiersConfig{ Verifiers: []config.VerifierConfig{verifierConfig}, } - verifiers, err := CreateVerifiersFromConfig(verifiersConfig, "", "") + verifiers, err := CreateVerifiersFromConfig(verifiersConfig, pluginDir, "") if err != nil { t.Fatalf("create verifiers failed with err %v", err) diff --git a/pkg/verifier/plugin/skel/skel_test.go b/pkg/verifier/plugin/skel/skel_test.go index 9367c40ed..3e2e4cd7f 100644 --- a/pkg/verifier/plugin/skel/skel_test.go +++ b/pkg/verifier/plugin/skel/skel_test.go @@ -18,6 +18,8 @@ package skel import ( "bytes" "fmt" + "os" + "path/filepath" "reflect" "strings" "testing" @@ -41,12 +43,12 @@ func TestPluginMain_VerifyReference_ReturnsExpected(t *testing.T) { t.Fatalf("expected artifact type %s actual %s", "test-type", referenceDescriptor.ArtifactType) } - if referrerStore.Name() != "test-store" { - t.Fatalf("expected store name %s actual %s", "test-store", referrerStore.Name()) + if referrerStore.Name() != "sample" { + t.Fatalf("expected store name %s actual %s", "sample", referrerStore.Name()) } // the parsed pluginBinDirs should include the data that was provided by Ratify, plus the default (currently assumed to be "") - expectedPluginBinDirs := []string{"/tmp/ratify/plugins", ""} + expectedPluginBinDirs := []string{getReferrerstorePluginsDir(), ""} pluginStore := referrerStore.(*sp.StorePlugin) actualPluginBinDirs := pluginStore.GetPath() if !reflect.DeepEqual(expectedPluginBinDirs, actualPluginBinDirs) { @@ -62,7 +64,7 @@ func TestPluginMain_VerifyReference_ReturnsExpected(t *testing.T) { plugin.SubjectEnvKey: "localhost:5000/net-monitor:v1@sha256:a0fc570a245b09ed752c42d600ee3bb5b4f77bbd70d8898780b7ab43454530eb", } - stdinData := `{ "storeConfig" : {"store": {"name":"test-store", "some": "config"}, "pluginBinDirs": ["/tmp/ratify/plugins"]}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}` + stdinData := fmt.Sprintf(`{ "storeConfig" : {"store": {"name":"sample", "some": "config"}, "pluginBinDirs": ["%s"]}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}`, getReferrerstorePluginsDir()) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} pluginContext := &pcontext{ @@ -129,7 +131,7 @@ func TestPluginMain_ErrorCases(t *testing.T) { plugin.SubjectEnvKey: "localhost:5000/net-monitor:v1@sha256:a0fc570a245b09ed752c42d600ee3bb5b4f77bbd70d8898780b7ab43454530eb", } - stdinData := `{ "storeConfig" : {"store": {"name":"test-store", "some": "config"}}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}` + stdinData := fmt.Sprintf(`{ "storeConfig" : {"store": {"name":"sample", "some": "config"}}, "pluginBinDirs": ["%s"], "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}`, getReferrerstorePluginsDir()) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} pluginContext := &pcontext{ @@ -161,14 +163,14 @@ func TestPluginMain_ErrorCases(t *testing.T) { environment[plugin.VersionEnvKey] = "1.0.0" - stdinData = `"storeConfig" : {"store": {"name":"test-store", "some": "config"}}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}` + stdinData = fmt.Sprintf(`"storeConfig" : {"store": {"name":"sample", "some": "config"}, "pluginBinDirs": ["%s"]},"config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}`, getReferrerstorePluginsDir()) pluginContext.Stdin = strings.NewReader(stdinData) err = pluginContext.pluginMainCore("skel-test-case", "1.0.0", verifyReference, []string{"1.0.0"}) if err == nil || err.Code != types.ErrConfigParsingFailure { t.Fatalf("plugin execution expected to fail with error code %d for invalid config", types.ErrConfigParsingFailure) } - stdinData = `{"storeConfig" : {"store": {"name":"test-store", "some": "config"}}, "config": {"some":"config"}, "referenceDesc": {"artifactType": "test-type"}}` + stdinData = fmt.Sprintf(`{"storeConfig" : {"store": {"name":"sample", "some": "config"}, "pluginBinDirs": ["%s"]}, "config": {"some":"config"}, "referenceDesc": {"artifactType": "test-type"}}`, getReferrerstorePluginsDir()) pluginContext.Stdin = strings.NewReader(stdinData) err = pluginContext.pluginMainCore("skel-test-case", "1.0.0", verifyReference, []string{"1.0.0"}) if err == nil || err.Code != types.ErrInvalidVerifierConfig { @@ -176,18 +178,24 @@ func TestPluginMain_ErrorCases(t *testing.T) { } environment[plugin.CommandEnvKey] = "unknown" - stdinData = `{"storeConfig" : {"store": {"name":"test-store", "some": "config"}}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}` + stdinData = fmt.Sprintf(`{"storeConfig" : {"store": {"name":"sample", "some": "config"}, "pluginBinDirs": ["%s"]}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}`, getReferrerstorePluginsDir()) pluginContext.Stdin = strings.NewReader(stdinData) err = pluginContext.pluginMainCore("skel-test-case", "1.0.0", verifyReference, []string{"1.0.0"}) if err == nil || err.Code != types.ErrUnknownCommand { - t.Fatalf("plugin execution expected to fail with error code %d for invalid command", types.ErrUnknownCommand) + t.Fatalf("plugin execution expected to fail with error code %d for invalid command, actual err :%v", types.ErrUnknownCommand, err) } environment[plugin.CommandEnvKey] = plugin.VerifyCommand - stdinData = `{"storeConfig" : {"store": {"name":"test-store", "some": "config"}}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}` + stdinData = fmt.Sprintf(`{"storeConfig" : {"store": {"name":"sample", "some": "config"}, "pluginBinDirs": ["%s"]}, "config": {"name": "skel-test-case", "some":"config"}, "referenceDesc": {"artifactType": "test-type"}}`, getReferrerstorePluginsDir()) pluginContext.Stdin = strings.NewReader(stdinData) err = pluginContext.pluginMainCore("skel-test-case", "1.0.0", verifyReference, []string{"1.0.0"}) if err == nil || err.Code != types.ErrPluginCmdFailure { t.Fatalf("plugin execution expected to fail with error code %d for cmd failure", types.ErrPluginCmdFailure) } } + +func getReferrerstorePluginsDir() string { + workingDir, _ := os.Getwd() + pluginDir := filepath.Clean(filepath.Join(workingDir, "../../../../", "./bin/plugins/referrerstore/")) + return pluginDir +} diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index 45fae7cba..e9671c3d2 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -166,6 +166,50 @@ RATIFY_NAMESPACE=gatekeeper-system assert_success } +@test "verifier crd status check" { + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete verifiers.config.ratify.deislabs.io/verifier-license-checker' + } + + # apply a valid verifier, validate status property shows success + run kubectl apply -f ./config/samples/config_v1beta1_verifier_complete_licensechecker.yaml + assert_success + run bash -c "kubectl describe verifiers.config.ratify.deislabs.io/verifier-license-checker -n ${RATIFY_NAMESPACE} | grep 'Issuccess: true'" + assert_success + + # apply a invalid verifier CR, validate status with error + sed 's/licensechecker/invalidlicensechecker/' ./config/samples/config_v1beta1_verifier_complete_licensechecker.yaml > invalidVerifier.yaml + run kubectl apply -f invalidVerifier.yaml + assert_success + run bash -c "kubectl describe verifiers.config.ratify.deislabs.io/verifier-license-checker -n ${RATIFY_NAMESPACE} | grep 'Brieferror: Original Error:'" + assert_success + + # apply a valid verifier, validate status property shows success + run kubectl apply -f ./config/samples/config_v1beta1_verifier_complete_licensechecker.yaml + assert_success + run bash -c "kubectl describe verifiers.config.ratify.deislabs.io/verifier-license-checker -n ${RATIFY_NAMESPACE} | grep 'Issuccess: true'" + assert_success + run bash -c "kubectl describe verifiers.config.ratify.deislabs.io/verifier-license-checker -n ${RATIFY_NAMESPACE} | grep 'Brieferror: Original Error:'" + assert_failure +} + +@test "store crd status check" { + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete stores.config.ratify.deislabs.io/store-dynamic' + } + + # apply a invalid verifier CR, validate status with error + sed 's/:v1/:invalid/' ./config/samples/config_v1beta1_store_dynamic.yaml > invalidstore.yaml + run kubectl apply -f invalidstore.yaml + assert_success + # wait for download of image + sleep 5 + run bash -c "kubectl describe stores.config.ratify.deislabs.io/store-dynamic -n ${RATIFY_NAMESPACE} | grep 'plugin not found'" + assert_success +} + @test "configmap update test" { skip "Skipping test for now as we are no longer watching for configfile update in a K8s environment. This test ensures we are watching config file updates in a non-kub scenario" run kubectl apply -f ./library/default/template.yaml