From 76d6c7437c01ee73f95b1f966a28bf39b23e4f7f Mon Sep 17 00:00:00 2001 From: Binbin Li Date: Tue, 23 Apr 2024 06:55:16 +0000 Subject: [PATCH] feat: add NamespacedStore CRD --- PROJECT | 8 + api/unversioned/namespacedstore_types.go | 69 +++++ api/unversioned/zz_generated.deepcopy.go | 79 ++++++ api/v1beta1/namespacedstore_types.go | 85 ++++++ api/v1beta1/zz_generated.conversion.go | 146 ++++++++++ api/v1beta1/zz_generated.deepcopy.go | 95 +++++++ ...espacedstore-customresourcedefinition.yaml | 92 ++++++ .../ratify-manager-role-clusterrole.yaml | 26 ++ ...g.ratify.deislabs.io_namespacedstores.yaml | 92 ++++++ config/crd/kustomization.yaml | 5 +- .../cainjection_in_namespacedstores.yaml | 7 + .../patches/webhook_in_namespacedstores.yaml | 16 ++ config/rbac/namespacedstore_editor_role.yaml | 31 ++ config/rbac/namespacedstore_viewer_role.yaml | 27 ++ .../store}/config_v1beta1_store_dynamic.yaml | 0 .../store}/config_v1beta1_store_oras.yaml | 0 .../config_v1beta1_store_oras_http.yaml | 0 ...onfig_v1beta1_store_oras_k8secretAuth.yaml | 0 .../config_v1beta1_namespacedstore.yaml | 12 + .../store/config_v1beta1_store_dynamic.yaml | 8 + .../store/config_v1beta1_store_oras.yaml | 10 + .../store/config_v1beta1_store_oras_http.yaml | 11 + ...onfig_v1beta1_store_oras_k8secretAuth.yaml | 14 + .../{ => clusterresource}/store_controller.go | 61 +--- .../store_controller_test.go | 146 ++++++++-- .../namespaceresource/store_controller.go | 112 ++++++++ .../store_controller_test.go | 265 ++++++++++++++++++ pkg/controllers/utils/store.go | 68 +++++ pkg/controllers/utils/store_test.go | 105 +++++++ pkg/controllers/verifier_controller_test.go | 2 + pkg/customresources/referrerstores/api.go | 6 - pkg/customresources/referrerstores/stores.go | 50 ++-- .../referrerstores/stores_test.go | 15 +- pkg/manager/manager.go | 9 +- test/bats/base-test.bats | 6 +- test/bats/plugin-test.bats | 40 ++- 36 files changed, 1595 insertions(+), 123 deletions(-) create mode 100644 api/unversioned/namespacedstore_types.go create mode 100644 api/v1beta1/namespacedstore_types.go create mode 100644 charts/ratify/crds/namespacedstore-customresourcedefinition.yaml create mode 100644 config/crd/bases/config.ratify.deislabs.io_namespacedstores.yaml create mode 100644 config/crd/patches/cainjection_in_namespacedstores.yaml create mode 100644 config/crd/patches/webhook_in_namespacedstores.yaml create mode 100644 config/rbac/namespacedstore_editor_role.yaml create mode 100644 config/rbac/namespacedstore_viewer_role.yaml rename config/samples/{ => clustered/store}/config_v1beta1_store_dynamic.yaml (100%) rename config/samples/{ => clustered/store}/config_v1beta1_store_oras.yaml (100%) rename config/samples/{ => clustered/store}/config_v1beta1_store_oras_http.yaml (100%) rename config/samples/{ => clustered/store}/config_v1beta1_store_oras_k8secretAuth.yaml (100%) create mode 100644 config/samples/config_v1beta1_namespacedstore.yaml create mode 100644 config/samples/namespaced/store/config_v1beta1_store_dynamic.yaml create mode 100644 config/samples/namespaced/store/config_v1beta1_store_oras.yaml create mode 100644 config/samples/namespaced/store/config_v1beta1_store_oras_http.yaml create mode 100644 config/samples/namespaced/store/config_v1beta1_store_oras_k8secretAuth.yaml rename pkg/controllers/{ => clusterresource}/store_controller.go (61%) rename pkg/controllers/{ => clusterresource}/store_controller_test.go (51%) create mode 100644 pkg/controllers/namespaceresource/store_controller.go create mode 100644 pkg/controllers/namespaceresource/store_controller_test.go create mode 100644 pkg/controllers/utils/store.go create mode 100644 pkg/controllers/utils/store_test.go diff --git a/PROJECT b/PROJECT index b91527b19b..418f446747 100644 --- a/PROJECT +++ b/PROJECT @@ -88,4 +88,12 @@ resources: kind: NamespacedPolicy path: github.com/deislabs/ratify/api/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + domain: ratify.deislabs.io + group: config + kind: NamespacedStore + path: github.com/deislabs/ratify/api/v1beta1 + version: v1beta1 version: "3" diff --git a/api/unversioned/namespacedstore_types.go b/api/unversioned/namespacedstore_types.go new file mode 100644 index 0000000000..c918da3c8a --- /dev/null +++ b/api/unversioned/namespacedstore_types.go @@ -0,0 +1,69 @@ +/* +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 unversioned + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// NamespacedStoreSpec defines the desired state of NamespacedStore +type NamespacedStoreSpec struct { + // Important: Run "make install-crds" to regenerate code after modifying this file + + // Name of the store + Name string `json:"name"` + // Version of the store plugin. Optional + Version string `json:"version,omitempty"` + // Plugin path, optional + Address string `json:"address,omitempty"` + // OCI Artifact source to download the plugin from, optional + Source *PluginSource `json:"source,omitempty"` + + // Parameters of the store + Parameters runtime.RawExtension `json:"parameters,omitempty"` +} + +// NamespacedStoreStatus defines the observed state of NamespacedStore +type NamespacedStoreStatus struct { + // 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"` +} + +// NamespacedStore is the Schema for the namespacedstores API +type NamespacedStore struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NamespacedStoreSpec `json:"spec,omitempty"` + Status NamespacedStoreStatus `json:"status,omitempty"` +} + +// NamespacedStoreList contains a list of NamespacedStore +type NamespacedStoreList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NamespacedStore `json:"items"` +} diff --git a/api/unversioned/zz_generated.deepcopy.go b/api/unversioned/zz_generated.deepcopy.go index 5e89a6dcba..ccf9bbeeec 100644 --- a/api/unversioned/zz_generated.deepcopy.go +++ b/api/unversioned/zz_generated.deepcopy.go @@ -255,6 +255,85 @@ func (in *NamespacedPolicyStatus) DeepCopy() *NamespacedPolicyStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStore) DeepCopyInto(out *NamespacedStore) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStore. +func (in *NamespacedStore) DeepCopy() *NamespacedStore { + if in == nil { + return nil + } + out := new(NamespacedStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStoreList) DeepCopyInto(out *NamespacedStoreList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NamespacedStore, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStoreList. +func (in *NamespacedStoreList) DeepCopy() *NamespacedStoreList { + if in == nil { + return nil + } + out := new(NamespacedStoreList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStoreSpec) DeepCopyInto(out *NamespacedStoreSpec) { + *out = *in + if in.Source != nil { + in, out := &in.Source, &out.Source + *out = new(PluginSource) + (*in).DeepCopyInto(*out) + } + in.Parameters.DeepCopyInto(&out.Parameters) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStoreSpec. +func (in *NamespacedStoreSpec) DeepCopy() *NamespacedStoreSpec { + if in == nil { + return nil + } + out := new(NamespacedStoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStoreStatus) DeepCopyInto(out *NamespacedStoreStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStoreStatus. +func (in *NamespacedStoreStatus) DeepCopy() *NamespacedStoreStatus { + if in == nil { + return nil + } + out := new(NamespacedStoreStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PluginSource) DeepCopyInto(out *PluginSource) { *out = *in diff --git a/api/v1beta1/namespacedstore_types.go b/api/v1beta1/namespacedstore_types.go new file mode 100644 index 0000000000..45a829087d --- /dev/null +++ b/api/v1beta1/namespacedstore_types.go @@ -0,0 +1,85 @@ +/* +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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// NamespacedStoreSpec defines the desired state of NamespacedStore +type NamespacedStoreSpec struct { + // Important: Run "make install-crds" to regenerate code after modifying this file + + // Name of the store + Name string `json:"name"` + // Version of the store plugin. Optional + Version string `json:"version,omitempty"` + // Plugin path, optional + Address string `json:"address,omitempty"` + // OCI Artifact source to download the plugin from, optional + Source *PluginSource `json:"source,omitempty"` + + // +kubebuilder:pruning:PreserveUnknownFields + // Parameters of the store + Parameters runtime.RawExtension `json:"parameters,omitempty"` +} + +// NamespacedStoreStatus defines the observed state of NamespacedStore +type NamespacedStoreStatus struct { + // 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="Namespaced" +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="IsSuccess",type=boolean,JSONPath=`.status.issuccess` +// +kubebuilder:printcolumn:name="Error",type=string,JSONPath=`.status.brieferror` +// NamespacedStore is the Schema for the namespacedstores API +type NamespacedStore struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NamespacedStoreSpec `json:"spec,omitempty"` + Status NamespacedStoreStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// NamespacedStoreList contains a list of NamespacedStore +type NamespacedStoreList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NamespacedStore `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NamespacedStore{}, &NamespacedStoreList{}) +} diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 2a98f8ac7d..b5d5b9c127 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -156,6 +156,46 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*NamespacedStore)(nil), (*unversioned.NamespacedStore)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_NamespacedStore_To_unversioned_NamespacedStore(a.(*NamespacedStore), b.(*unversioned.NamespacedStore), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedStore)(nil), (*NamespacedStore)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_unversioned_NamespacedStore_To_v1beta1_NamespacedStore(a.(*unversioned.NamespacedStore), b.(*NamespacedStore), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*NamespacedStoreList)(nil), (*unversioned.NamespacedStoreList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_NamespacedStoreList_To_unversioned_NamespacedStoreList(a.(*NamespacedStoreList), b.(*unversioned.NamespacedStoreList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedStoreList)(nil), (*NamespacedStoreList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_unversioned_NamespacedStoreList_To_v1beta1_NamespacedStoreList(a.(*unversioned.NamespacedStoreList), b.(*NamespacedStoreList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*NamespacedStoreSpec)(nil), (*unversioned.NamespacedStoreSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_NamespacedStoreSpec_To_unversioned_NamespacedStoreSpec(a.(*NamespacedStoreSpec), b.(*unversioned.NamespacedStoreSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedStoreSpec)(nil), (*NamespacedStoreSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_unversioned_NamespacedStoreSpec_To_v1beta1_NamespacedStoreSpec(a.(*unversioned.NamespacedStoreSpec), b.(*NamespacedStoreSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*NamespacedStoreStatus)(nil), (*unversioned.NamespacedStoreStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_NamespacedStoreStatus_To_unversioned_NamespacedStoreStatus(a.(*NamespacedStoreStatus), b.(*unversioned.NamespacedStoreStatus), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedStoreStatus)(nil), (*NamespacedStoreStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_unversioned_NamespacedStoreStatus_To_v1beta1_NamespacedStoreStatus(a.(*unversioned.NamespacedStoreStatus), b.(*NamespacedStoreStatus), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*PluginSource)(nil), (*unversioned.PluginSource)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_PluginSource_To_unversioned_PluginSource(a.(*PluginSource), b.(*unversioned.PluginSource), scope) }); err != nil { @@ -597,6 +637,112 @@ func Convert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatu return autoConvert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatus(in, out, s) } +func autoConvert_v1beta1_NamespacedStore_To_unversioned_NamespacedStore(in *NamespacedStore, out *unversioned.NamespacedStore, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1beta1_NamespacedStoreSpec_To_unversioned_NamespacedStoreSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_v1beta1_NamespacedStoreStatus_To_unversioned_NamespacedStoreStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta1_NamespacedStore_To_unversioned_NamespacedStore is an autogenerated conversion function. +func Convert_v1beta1_NamespacedStore_To_unversioned_NamespacedStore(in *NamespacedStore, out *unversioned.NamespacedStore, s conversion.Scope) error { + return autoConvert_v1beta1_NamespacedStore_To_unversioned_NamespacedStore(in, out, s) +} + +func autoConvert_unversioned_NamespacedStore_To_v1beta1_NamespacedStore(in *unversioned.NamespacedStore, out *NamespacedStore, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_unversioned_NamespacedStoreSpec_To_v1beta1_NamespacedStoreSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_unversioned_NamespacedStoreStatus_To_v1beta1_NamespacedStoreStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_unversioned_NamespacedStore_To_v1beta1_NamespacedStore is an autogenerated conversion function. +func Convert_unversioned_NamespacedStore_To_v1beta1_NamespacedStore(in *unversioned.NamespacedStore, out *NamespacedStore, s conversion.Scope) error { + return autoConvert_unversioned_NamespacedStore_To_v1beta1_NamespacedStore(in, out, s) +} + +func autoConvert_v1beta1_NamespacedStoreList_To_unversioned_NamespacedStoreList(in *NamespacedStoreList, out *unversioned.NamespacedStoreList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]unversioned.NamespacedStore)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1beta1_NamespacedStoreList_To_unversioned_NamespacedStoreList is an autogenerated conversion function. +func Convert_v1beta1_NamespacedStoreList_To_unversioned_NamespacedStoreList(in *NamespacedStoreList, out *unversioned.NamespacedStoreList, s conversion.Scope) error { + return autoConvert_v1beta1_NamespacedStoreList_To_unversioned_NamespacedStoreList(in, out, s) +} + +func autoConvert_unversioned_NamespacedStoreList_To_v1beta1_NamespacedStoreList(in *unversioned.NamespacedStoreList, out *NamespacedStoreList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]NamespacedStore)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_unversioned_NamespacedStoreList_To_v1beta1_NamespacedStoreList is an autogenerated conversion function. +func Convert_unversioned_NamespacedStoreList_To_v1beta1_NamespacedStoreList(in *unversioned.NamespacedStoreList, out *NamespacedStoreList, s conversion.Scope) error { + return autoConvert_unversioned_NamespacedStoreList_To_v1beta1_NamespacedStoreList(in, out, s) +} + +func autoConvert_v1beta1_NamespacedStoreSpec_To_unversioned_NamespacedStoreSpec(in *NamespacedStoreSpec, out *unversioned.NamespacedStoreSpec, s conversion.Scope) error { + out.Name = in.Name + out.Version = in.Version + out.Address = in.Address + out.Source = (*unversioned.PluginSource)(unsafe.Pointer(in.Source)) + out.Parameters = in.Parameters + return nil +} + +// Convert_v1beta1_NamespacedStoreSpec_To_unversioned_NamespacedStoreSpec is an autogenerated conversion function. +func Convert_v1beta1_NamespacedStoreSpec_To_unversioned_NamespacedStoreSpec(in *NamespacedStoreSpec, out *unversioned.NamespacedStoreSpec, s conversion.Scope) error { + return autoConvert_v1beta1_NamespacedStoreSpec_To_unversioned_NamespacedStoreSpec(in, out, s) +} + +func autoConvert_unversioned_NamespacedStoreSpec_To_v1beta1_NamespacedStoreSpec(in *unversioned.NamespacedStoreSpec, out *NamespacedStoreSpec, s conversion.Scope) error { + out.Name = in.Name + out.Version = in.Version + out.Address = in.Address + out.Source = (*PluginSource)(unsafe.Pointer(in.Source)) + out.Parameters = in.Parameters + return nil +} + +// Convert_unversioned_NamespacedStoreSpec_To_v1beta1_NamespacedStoreSpec is an autogenerated conversion function. +func Convert_unversioned_NamespacedStoreSpec_To_v1beta1_NamespacedStoreSpec(in *unversioned.NamespacedStoreSpec, out *NamespacedStoreSpec, s conversion.Scope) error { + return autoConvert_unversioned_NamespacedStoreSpec_To_v1beta1_NamespacedStoreSpec(in, out, s) +} + +func autoConvert_v1beta1_NamespacedStoreStatus_To_unversioned_NamespacedStoreStatus(in *NamespacedStoreStatus, out *unversioned.NamespacedStoreStatus, s conversion.Scope) error { + out.IsSuccess = in.IsSuccess + out.Error = in.Error + out.BriefError = in.BriefError + return nil +} + +// Convert_v1beta1_NamespacedStoreStatus_To_unversioned_NamespacedStoreStatus is an autogenerated conversion function. +func Convert_v1beta1_NamespacedStoreStatus_To_unversioned_NamespacedStoreStatus(in *NamespacedStoreStatus, out *unversioned.NamespacedStoreStatus, s conversion.Scope) error { + return autoConvert_v1beta1_NamespacedStoreStatus_To_unversioned_NamespacedStoreStatus(in, out, s) +} + +func autoConvert_unversioned_NamespacedStoreStatus_To_v1beta1_NamespacedStoreStatus(in *unversioned.NamespacedStoreStatus, out *NamespacedStoreStatus, s conversion.Scope) error { + out.IsSuccess = in.IsSuccess + out.Error = in.Error + out.BriefError = in.BriefError + return nil +} + +// Convert_unversioned_NamespacedStoreStatus_To_v1beta1_NamespacedStoreStatus is an autogenerated conversion function. +func Convert_unversioned_NamespacedStoreStatus_To_v1beta1_NamespacedStoreStatus(in *unversioned.NamespacedStoreStatus, out *NamespacedStoreStatus, s conversion.Scope) error { + return autoConvert_unversioned_NamespacedStoreStatus_To_v1beta1_NamespacedStoreStatus(in, out, s) +} + func autoConvert_v1beta1_PluginSource_To_unversioned_PluginSource(in *PluginSource, out *unversioned.PluginSource, s conversion.Scope) error { out.Artifact = in.Artifact out.AuthProvider = in.AuthProvider diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 076806a98c..49342bd520 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -305,6 +305,101 @@ func (in *NamespacedPolicyStatus) DeepCopy() *NamespacedPolicyStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStore) DeepCopyInto(out *NamespacedStore) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStore. +func (in *NamespacedStore) DeepCopy() *NamespacedStore { + if in == nil { + return nil + } + out := new(NamespacedStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NamespacedStore) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStoreList) DeepCopyInto(out *NamespacedStoreList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NamespacedStore, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStoreList. +func (in *NamespacedStoreList) DeepCopy() *NamespacedStoreList { + if in == nil { + return nil + } + out := new(NamespacedStoreList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NamespacedStoreList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStoreSpec) DeepCopyInto(out *NamespacedStoreSpec) { + *out = *in + if in.Source != nil { + in, out := &in.Source, &out.Source + *out = new(PluginSource) + (*in).DeepCopyInto(*out) + } + in.Parameters.DeepCopyInto(&out.Parameters) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStoreSpec. +func (in *NamespacedStoreSpec) DeepCopy() *NamespacedStoreSpec { + if in == nil { + return nil + } + out := new(NamespacedStoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedStoreStatus) DeepCopyInto(out *NamespacedStoreStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedStoreStatus. +func (in *NamespacedStoreStatus) DeepCopy() *NamespacedStoreStatus { + if in == nil { + return nil + } + out := new(NamespacedStoreStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PluginSource) DeepCopyInto(out *PluginSource) { *out = *in diff --git a/charts/ratify/crds/namespacedstore-customresourcedefinition.yaml b/charts/ratify/crds/namespacedstore-customresourcedefinition.yaml new file mode 100644 index 0000000000..610929a044 --- /dev/null +++ b/charts/ratify/crds/namespacedstore-customresourcedefinition.yaml @@ -0,0 +1,92 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: namespacedstores.config.ratify.deislabs.io +spec: + group: config.ratify.deislabs.io + names: + kind: NamespacedStore + listKind: NamespacedStoreList + plural: namespacedstores + singular: namespacedstore + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: NamespacedStore is the Schema for the namespacedstores 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: NamespacedStoreSpec defines the desired state of NamespacedStore + properties: + address: + description: Plugin path, optional + type: string + name: + description: Name of the store + type: string + parameters: + description: Parameters of the store + type: object + x-kubernetes-preserve-unknown-fields: true + source: + description: OCI Artifact source to download the plugin from, optional + properties: + artifact: + description: OCI Artifact source to download the plugin from + type: string + authProvider: + description: AuthProvider to use to authenticate to the OCI Artifact + source, optional + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + version: + description: Version of the store plugin. Optional + type: string + required: + - name + type: object + status: + description: NamespacedStoreStatus defines the observed state of NamespacedStore + 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/templates/ratify-manager-role-clusterrole.yaml b/charts/ratify/templates/ratify-manager-role-clusterrole.yaml index a2bd679b61..14bf0344ff 100644 --- a/charts/ratify/templates/ratify-manager-role-clusterrole.yaml +++ b/charts/ratify/templates/ratify-manager-role-clusterrole.yaml @@ -31,6 +31,32 @@ rules: - get - patch - update +- apiGroups: + - config.ratify.deislabs.io + resources: + - namespacedstores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - config.ratify.deislabs.io + resources: + - namespacedstores/finalizers + verbs: + - update +- apiGroups: + - config.ratify.deislabs.io + resources: + - namespacedstores/status + verbs: + - get + - patch + - update - apiGroups: - config.ratify.deislabs.io resources: diff --git a/config/crd/bases/config.ratify.deislabs.io_namespacedstores.yaml b/config/crd/bases/config.ratify.deislabs.io_namespacedstores.yaml new file mode 100644 index 0000000000..610929a044 --- /dev/null +++ b/config/crd/bases/config.ratify.deislabs.io_namespacedstores.yaml @@ -0,0 +1,92 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: namespacedstores.config.ratify.deislabs.io +spec: + group: config.ratify.deislabs.io + names: + kind: NamespacedStore + listKind: NamespacedStoreList + plural: namespacedstores + singular: namespacedstore + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: NamespacedStore is the Schema for the namespacedstores 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: NamespacedStoreSpec defines the desired state of NamespacedStore + properties: + address: + description: Plugin path, optional + type: string + name: + description: Name of the store + type: string + parameters: + description: Parameters of the store + type: object + x-kubernetes-preserve-unknown-fields: true + source: + description: OCI Artifact source to download the plugin from, optional + properties: + artifact: + description: OCI Artifact source to download the plugin from + type: string + authProvider: + description: AuthProvider to use to authenticate to the OCI Artifact + source, optional + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + version: + description: Version of the store plugin. Optional + type: string + required: + - name + type: object + status: + description: NamespacedStoreStatus defines the observed state of NamespacedStore + 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/kustomization.yaml b/config/crd/kustomization.yaml index 5ade6da935..4e4cf12579 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -8,6 +8,7 @@ resources: - bases/config.ratify.deislabs.io_policies.yaml - bases/config.ratify.deislabs.io_keymanagementproviders.yaml - bases/config.ratify.deislabs.io_namespacedpolicies.yaml + - bases/config.ratify.deislabs.io_namespacedstores.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -19,6 +20,7 @@ patchesStrategicMerge: #- patches/webhook_in_policies.yaml #- patches/webhook_in_keymanagementproviders.yaml #- patches/webhook_in_namespacedpolicies.yaml + #- patches/webhook_in_namespacedstores.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -29,8 +31,9 @@ patchesStrategicMerge: #- patches/cainjection_in_policies.yaml #- patches/cainjection_in_keymanagementproviders.yaml #- patches/cainjection_in_namespacedpolicies.yaml + #- patches/cainjection_in_namespacedstores.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. configurations: -- kustomizeconfig.yaml + - kustomizeconfig.yaml diff --git a/config/crd/patches/cainjection_in_namespacedstores.yaml b/config/crd/patches/cainjection_in_namespacedstores.yaml new file mode 100644 index 0000000000..db60680334 --- /dev/null +++ b/config/crd/patches/cainjection_in_namespacedstores.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: namespacedstores.config.ratify.deislabs.io diff --git a/config/crd/patches/webhook_in_namespacedstores.yaml b/config/crd/patches/webhook_in_namespacedstores.yaml new file mode 100644 index 0000000000..f4ba8b69a2 --- /dev/null +++ b/config/crd/patches/webhook_in_namespacedstores.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: namespacedstores.config.ratify.deislabs.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/namespacedstore_editor_role.yaml b/config/rbac/namespacedstore_editor_role.yaml new file mode 100644 index 0000000000..6f983a8c30 --- /dev/null +++ b/config/rbac/namespacedstore_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit namespacedstores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: namespacedstore-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ratify + app.kubernetes.io/part-of: ratify + app.kubernetes.io/managed-by: kustomize + name: namespacedstore-editor-role +rules: +- apiGroups: + - config.ratify.deislabs.io + resources: + - namespacedstores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - config.ratify.deislabs.io + resources: + - namespacedstores/status + verbs: + - get diff --git a/config/rbac/namespacedstore_viewer_role.yaml b/config/rbac/namespacedstore_viewer_role.yaml new file mode 100644 index 0000000000..f3b276be8b --- /dev/null +++ b/config/rbac/namespacedstore_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view namespacedstores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: namespacedstore-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ratify + app.kubernetes.io/part-of: ratify + app.kubernetes.io/managed-by: kustomize + name: namespacedstore-viewer-role +rules: +- apiGroups: + - config.ratify.deislabs.io + resources: + - namespacedstores + verbs: + - get + - list + - watch +- apiGroups: + - config.ratify.deislabs.io + resources: + - namespacedstores/status + verbs: + - get diff --git a/config/samples/config_v1beta1_store_dynamic.yaml b/config/samples/clustered/store/config_v1beta1_store_dynamic.yaml similarity index 100% rename from config/samples/config_v1beta1_store_dynamic.yaml rename to config/samples/clustered/store/config_v1beta1_store_dynamic.yaml diff --git a/config/samples/config_v1beta1_store_oras.yaml b/config/samples/clustered/store/config_v1beta1_store_oras.yaml similarity index 100% rename from config/samples/config_v1beta1_store_oras.yaml rename to config/samples/clustered/store/config_v1beta1_store_oras.yaml diff --git a/config/samples/config_v1beta1_store_oras_http.yaml b/config/samples/clustered/store/config_v1beta1_store_oras_http.yaml similarity index 100% rename from config/samples/config_v1beta1_store_oras_http.yaml rename to config/samples/clustered/store/config_v1beta1_store_oras_http.yaml diff --git a/config/samples/config_v1beta1_store_oras_k8secretAuth.yaml b/config/samples/clustered/store/config_v1beta1_store_oras_k8secretAuth.yaml similarity index 100% rename from config/samples/config_v1beta1_store_oras_k8secretAuth.yaml rename to config/samples/clustered/store/config_v1beta1_store_oras_k8secretAuth.yaml diff --git a/config/samples/config_v1beta1_namespacedstore.yaml b/config/samples/config_v1beta1_namespacedstore.yaml new file mode 100644 index 0000000000..677f45d478 --- /dev/null +++ b/config/samples/config_v1beta1_namespacedstore.yaml @@ -0,0 +1,12 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: NamespacedStore +metadata: + labels: + app.kubernetes.io/name: namespacedstore + app.kubernetes.io/instance: namespacedstore-sample + app.kubernetes.io/part-of: ratify + app.kuberentes.io/managed-by: kustomize + app.kubernetes.io/created-by: ratify + name: namespacedstore-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/namespaced/store/config_v1beta1_store_dynamic.yaml b/config/samples/namespaced/store/config_v1beta1_store_dynamic.yaml new file mode 100644 index 0000000000..1e99263455 --- /dev/null +++ b/config/samples/namespaced/store/config_v1beta1_store_dynamic.yaml @@ -0,0 +1,8 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: NamespacedStore +metadata: + name: store-dynamic +spec: + name: dynamic + source: + artifact: wabbitnetworks.azurecr.io/test/sample-store-plugin:v1 diff --git a/config/samples/namespaced/store/config_v1beta1_store_oras.yaml b/config/samples/namespaced/store/config_v1beta1_store_oras.yaml new file mode 100644 index 0000000000..d9774331e7 --- /dev/null +++ b/config/samples/namespaced/store/config_v1beta1_store_oras.yaml @@ -0,0 +1,10 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: NamespacedStore +metadata: + name: store-oras +spec: + name: oras + parameters: + cacheEnabled: true + cosignEnabled: true + ttl: 10 diff --git a/config/samples/namespaced/store/config_v1beta1_store_oras_http.yaml b/config/samples/namespaced/store/config_v1beta1_store_oras_http.yaml new file mode 100644 index 0000000000..dfd1bc3908 --- /dev/null +++ b/config/samples/namespaced/store/config_v1beta1_store_oras_http.yaml @@ -0,0 +1,11 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: NamespacedStore +metadata: + name: store-oras +spec: + name: oras + parameters: + cacheEnabled: true + cosignEnabled: true + ttl: 10 + useHttp: true \ No newline at end of file diff --git a/config/samples/namespaced/store/config_v1beta1_store_oras_k8secretAuth.yaml b/config/samples/namespaced/store/config_v1beta1_store_oras_k8secretAuth.yaml new file mode 100644 index 0000000000..965bced51e --- /dev/null +++ b/config/samples/namespaced/store/config_v1beta1_store_oras_k8secretAuth.yaml @@ -0,0 +1,14 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: NamespacedStore +metadata: + name: store-oras +spec: + name: oras + parameters: + cacheEnabled: true + ttl: 10 + useHttp: true + authProvider: + name: k8Secrets + secrets: + - secretName: ratify-dockerconfig \ No newline at end of file diff --git a/pkg/controllers/store_controller.go b/pkg/controllers/clusterresource/store_controller.go similarity index 61% rename from pkg/controllers/store_controller.go rename to pkg/controllers/clusterresource/store_controller.go index 6349818af2..b9cfac901a 100644 --- a/pkg/controllers/store_controller.go +++ b/pkg/controllers/clusterresource/store_controller.go @@ -13,11 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package clusterresource import ( "context" - "encoding/json" "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -26,11 +25,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" configv1beta1 "github.com/deislabs/ratify/api/v1beta1" - "github.com/deislabs/ratify/config" "github.com/deislabs/ratify/internal/constants" - rc "github.com/deislabs/ratify/pkg/referrerstore/config" - sf "github.com/deislabs/ratify/pkg/referrerstore/factory" - "github.com/deislabs/ratify/pkg/referrerstore/types" + "github.com/deislabs/ratify/pkg/controllers" + "github.com/deislabs/ratify/pkg/controllers/utils" "github.com/sirupsen/logrus" ) @@ -54,13 +51,12 @@ func (r *StoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl var store configv1beta1.Store var resource = req.Name - storeLogger.Infof("reconciling store '%v'", resource) + storeLogger.Infof("reconciling cluster store '%v'", resource) if err := r.Get(ctx, req.NamespacedName, &store); err != nil { if apierrors.IsNotFound(err) { storeLogger.Infof("deletion detected, removing store %v", req.Name) - // TODO: pass the actual namespace once multi-tenancy is supported. - NamespacedStores.DeleteStore(constants.EmptyNamespace, resource) + controllers.NamespacedStores.DeleteStore(constants.EmptyNamespace, resource) } else { storeLogger.Error(err, "unable to fetch store") } @@ -89,51 +85,12 @@ func (r *StoreReconciler) SetupWithManager(mgr ctrl.Manager) error { // Creates a store reference from CRD spec and add store to map func storeAddOrReplace(spec configv1beta1.StoreSpec, fullname string) error { - storeConfig, err := specToStoreConfig(spec) + storeConfig, err := utils.CreateStoreConfig(spec.Parameters.Raw, spec.Name, spec.Source) if err != nil { return fmt.Errorf("unable to convert store spec to store config, err: %w", err) } - // if the default version is not suitable, the plugin configuration should specify the desired version - if len(spec.Version) == 0 { - spec.Version = config.GetDefaultPluginVersion() - logrus.Infof("Version was empty, setting to default version: %v", spec.Version) - } - - if spec.Address == "" { - spec.Address = config.GetDefaultPluginPath() - logrus.Infof("Address was empty, setting to default path %v", spec.Address) - } - storeReference, err := sf.CreateStoreFromConfig(storeConfig, spec.Version, []string{spec.Address}) - - if err != nil || storeReference == nil { - logrus.Error(err, "store factory failed to create store from store config") - return fmt.Errorf("store factory failed to create store from store config, err: %w", err) - } - - // TODO: pass the actual namespace once multi-tenancy is supported. - NamespacedStores.AddStore(constants.EmptyNamespace, fullname, storeReference) - logrus.Infof("store '%v' added to store map", storeReference.Name()) - - return nil -} - -// Returns a store reference from spec -func specToStoreConfig(storeSpec configv1beta1.StoreSpec) (rc.StorePluginConfig, error) { - storeConfig := rc.StorePluginConfig{} - - if string(storeSpec.Parameters.Raw) != "" { - if err := json.Unmarshal(storeSpec.Parameters.Raw, &storeConfig); err != nil { - logrus.Error(err, "unable to decode store parameters", "Parameters.Raw", storeSpec.Parameters.Raw) - return rc.StorePluginConfig{}, err - } - } - storeConfig[types.Name] = storeSpec.Name - if storeSpec.Source != nil { - storeConfig[types.Source] = storeSpec.Source - } - - return storeConfig, nil + return utils.UpsertStoreMap(spec.Version, spec.Address, fullname, constants.EmptyNamespace, storeConfig) } func writeStoreStatus(ctx context.Context, r client.StatusClient, store *configv1beta1.Store, logger *logrus.Entry, isSuccess bool, errorString string) { @@ -144,8 +101,8 @@ func writeStoreStatus(ctx context.Context, r client.StatusClient, store *configv } else { store.Status.IsSuccess = false store.Status.Error = errorString - if len(errorString) > maxBriefErrLength { - store.Status.BriefError = fmt.Sprintf("%s...", errorString[:maxBriefErrLength]) + if len(errorString) > constants.MaxBriefErrLength { + store.Status.BriefError = fmt.Sprintf("%s...", errorString[:constants.MaxBriefErrLength]) } } diff --git a/pkg/controllers/store_controller_test.go b/pkg/controllers/clusterresource/store_controller_test.go similarity index 51% rename from pkg/controllers/store_controller_test.go rename to pkg/controllers/clusterresource/store_controller_test.go index 1aa4084475..4123e5e4cd 100644 --- a/pkg/controllers/store_controller_test.go +++ b/pkg/controllers/clusterresource/store_controller_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package clusterresource import ( "context" @@ -23,14 +23,25 @@ import ( configv1beta1 "github.com/deislabs/ratify/api/v1beta1" "github.com/deislabs/ratify/internal/constants" + "github.com/deislabs/ratify/pkg/controllers" rs "github.com/deislabs/ratify/pkg/customresources/referrerstores" "github.com/deislabs/ratify/pkg/utils" + test "github.com/deislabs/ratify/pkg/utils" "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -const sampleName = "sample" +const ( + storeName = "testStore" + testNamespace = "testNamespace" + sampleName = "sample" + orasName = "oras" +) func TestStoreAdd_EmptyParameter(t *testing.T) { resetStoreMap() @@ -48,15 +59,16 @@ func TestStoreAdd_EmptyParameter(t *testing.T) { if err := storeAddOrReplace(testStoreSpec, "oras"); err != nil { t.Fatalf("storeAddOrReplace() expected no error, actual %v", err) } - if NamespacedStores.GetStoreCount() != 1 { - t.Fatalf("Store map expected size 1, actual %v", NamespacedStores.GetStoreCount()) + stores := controllers.NamespacedStores.GetStores(constants.EmptyNamespace) + if len(stores) != 1 { + t.Fatalf("Store map expected size 1, actual %v", len(stores)) } } func TestStoreAdd_WithParameters(t *testing.T) { resetStoreMap() - if NamespacedStores.GetStoreCount() != 0 { - t.Fatalf("Store map expected size 0, actual %v", NamespacedStores.GetStoreCount()) + if len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace)) != 0 { + t.Fatalf("Store map expected size 0, actual %v", len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace))) } dirPath, err := utils.CreatePlugin(sampleName) if err != nil { @@ -69,8 +81,8 @@ func TestStoreAdd_WithParameters(t *testing.T) { if err := storeAddOrReplace(testStoreSpec, "testObject"); err != nil { t.Fatalf("storeAddOrReplace() expected no error, actual %v", err) } - if NamespacedStores.GetStoreCount() != 1 { - t.Fatalf("Store map expected size 1, actual %v", NamespacedStores.GetStoreCount()) + if len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace)) != 1 { + t.Fatalf("Store map expected size 1, actual %v", len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace))) } } @@ -87,21 +99,21 @@ func TestWriteStoreStatus(t *testing.T) { name: "success status", isSuccess: true, store: &configv1beta1.Store{}, - reconciler: &mockStatusClient{}, + reconciler: &test.MockStatusClient{}, }, { name: "error status", isSuccess: false, store: &configv1beta1.Store{}, errString: "a long error string that exceeds the max length of 30 characters", - reconciler: &mockStatusClient{}, + reconciler: &test.MockStatusClient{}, }, { name: "status update failed", isSuccess: true, store: &configv1beta1.Store{}, - reconciler: &mockStatusClient{ - updateFailed: true, + reconciler: &test.MockStatusClient{ + UpdateFailed: true, }, }, } @@ -138,8 +150,8 @@ func TestStore_UpdateAndDelete(t *testing.T) { if err := storeAddOrReplace(testStoreSpec, sampleName); err != nil { t.Fatalf("storeAddOrReplace() expected no error, actual %v", err) } - if NamespacedStores.GetStoreCount() != 1 { - t.Fatalf("Store map expected size 1, actual %v", NamespacedStores.GetStoreCount()) + if len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace)) != 1 { + t.Fatalf("Store map expected size 1, actual %v", len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace))) } // modify the Store @@ -153,19 +165,113 @@ func TestStore_UpdateAndDelete(t *testing.T) { } // validate no Store has been added - if NamespacedStores.GetStoreCount() != 1 { - t.Fatalf("Store map should be 1 after replacement, actual %v", NamespacedStores.GetStoreCount()) + if len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace)) != 1 { + t.Fatalf("Store map should be 1 after replacement, actual %v", len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace))) + } + + controllers.NamespacedStores.DeleteStore(constants.EmptyNamespace, sampleName) + + if len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace)) != 0 { + t.Fatalf("Store map should be 0 after deletion, actual %v", len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace))) + } +} + +func TestStoreReconcile(t *testing.T) { + dirPath, err := test.CreatePlugin(orasName) + if err != nil { + t.Fatalf("createPlugin() expected no error, actual %v", err) + } + defer os.RemoveAll(dirPath) + + tests := []struct { + name string + store *configv1beta1.Store + req *reconcile.Request + expectedErr bool + expectedStoreCount int + }{ + { + name: "nonexistent store", + req: &reconcile.Request{ + NamespacedName: types.NamespacedName{Name: "nonexistent"}, + }, + store: &configv1beta1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: constants.EmptyNamespace, + Name: storeName, + }, + Spec: configv1beta1.StoreSpec{ + Name: orasName, + }, + }, + expectedErr: false, + expectedStoreCount: 0, + }, + { + name: "valid spec", + store: &configv1beta1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: constants.EmptyNamespace, + Name: storeName, + }, + Spec: configv1beta1.StoreSpec{ + Name: orasName, + Address: dirPath, + }, + }, + expectedErr: false, + expectedStoreCount: 1, + }, + { + name: "invalid parameters", + store: &configv1beta1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: constants.EmptyNamespace, + Name: storeName, + }, + Spec: configv1beta1.StoreSpec{ + Parameters: runtime.RawExtension{ + Raw: []byte("test"), + }, + }, + }, + expectedErr: true, + expectedStoreCount: 0, + }, } - NamespacedStores.DeleteStore(constants.EmptyNamespace, sampleName) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetStoreMap() + scheme, _ := test.CreateScheme() + client := fake.NewClientBuilder().WithScheme(scheme) + client.WithObjects(tt.store) + r := &StoreReconciler{ + Scheme: scheme, + Client: client.Build(), + } + var req reconcile.Request + if tt.req != nil { + req = *tt.req + } else { + req = reconcile.Request{ + NamespacedName: test.KeyFor(tt.store), + } + } - if NamespacedStores.GetStoreCount() != 0 { - t.Fatalf("Store map should be 0 after deletion, actual %v", NamespacedStores.GetStoreCount()) + _, err := r.Reconcile(context.Background(), req) + if tt.expectedErr != (err != nil) { + t.Fatalf("Reconcile() expected error %v, actual %v", tt.expectedErr, err) + } + if len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace)) != tt.expectedStoreCount { + t.Fatalf("Store map expected size %v, actual %v", tt.expectedStoreCount, len(controllers.NamespacedStores.GetStores(constants.EmptyNamespace))) + } + }) } } func resetStoreMap() { - NamespacedStores = rs.NewActiveStores() + controllers.NamespacedStores = rs.NewActiveStores() } func getOrasStoreSpec(pluginName, pluginPath string) configv1beta1.StoreSpec { diff --git a/pkg/controllers/namespaceresource/store_controller.go b/pkg/controllers/namespaceresource/store_controller.go new file mode 100644 index 0000000000..b150de35e6 --- /dev/null +++ b/pkg/controllers/namespaceresource/store_controller.go @@ -0,0 +1,112 @@ +/* +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 namespaceresource + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + configv1beta1 "github.com/deislabs/ratify/api/v1beta1" + "github.com/deislabs/ratify/internal/constants" + "github.com/deislabs/ratify/pkg/controllers" + "github.com/deislabs/ratify/pkg/controllers/utils" + "github.com/sirupsen/logrus" +) + +// StoreReconciler reconciles a Store object +type StoreReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedstores,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedstores/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedstores/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile +func (r *StoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + storeLogger := logrus.WithContext(ctx) + + var store configv1beta1.NamespacedStore + var resource = req.Name + storeLogger.Infof("reconciling store '%v'", resource) + + if err := r.Get(ctx, req.NamespacedName, &store); err != nil { + if apierrors.IsNotFound(err) { + storeLogger.Infof("deletion detected, removing store %v", req.Name) + controllers.NamespacedStores.DeleteStore(req.Namespace, resource) + } else { + storeLogger.Error(err, "unable to fetch store") + } + + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if err := storeAddOrReplace(store.Spec, resource, req.Namespace); 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 +} + +// SetupWithManager sets up the controller with the Manager. +func (r *StoreReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&configv1beta1.NamespacedStore{}). + Complete(r) +} + +// Creates a store reference from CRD spec and add store to map +func storeAddOrReplace(spec configv1beta1.NamespacedStoreSpec, fullname, namespace string) error { + storeConfig, err := utils.CreateStoreConfig(spec.Parameters.Raw, spec.Name, spec.Source) + if err != nil { + return fmt.Errorf("unable to convert store spec to store config, err: %w", err) + } + + return utils.UpsertStoreMap(spec.Version, spec.Address, fullname, namespace, storeConfig) +} + +func writeStoreStatus(ctx context.Context, r client.StatusClient, store *configv1beta1.NamespacedStore, 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) > constants.MaxBriefErrLength { + store.Status.BriefError = fmt.Sprintf("%s...", errorString[:constants.MaxBriefErrLength]) + } + } + + if statusErr := r.Status().Update(ctx, store); statusErr != nil { + logger.Error(statusErr, ",unable to update store error status") + } +} diff --git a/pkg/controllers/namespaceresource/store_controller_test.go b/pkg/controllers/namespaceresource/store_controller_test.go new file mode 100644 index 0000000000..a66f84b47a --- /dev/null +++ b/pkg/controllers/namespaceresource/store_controller_test.go @@ -0,0 +1,265 @@ +/* +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 namespaceresource + +import ( + "context" + "os" + "strings" + "testing" + + configv1beta1 "github.com/deislabs/ratify/api/v1beta1" + "github.com/deislabs/ratify/pkg/controllers" + "github.com/deislabs/ratify/pkg/customresources/referrerstores" + test "github.com/deislabs/ratify/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + storeName = "testStore" + sampleName = "sample" + orasName = "oras" +) + +func TestStoreAdd_EmptyParameter(t *testing.T) { + resetStoreMap() + dirPath, err := test.CreatePlugin(sampleName) + if err != nil { + t.Fatalf("createPlugin() expected no error, actual %v", err) + } + defer os.RemoveAll(dirPath) + + var testStoreSpec = configv1beta1.NamespacedStoreSpec{ + Name: sampleName, + Address: dirPath, + } + + if err := storeAddOrReplace(testStoreSpec, "oras", testNamespace); err != nil { + t.Fatalf("storeAddOrReplace() expected no error, actual %v", err) + } + if len(controllers.NamespacedStores.GetStores(testNamespace)) != 1 { + t.Fatalf("Store map expected size 1, actual %v", len(controllers.NamespacedStores.GetStores(testNamespace))) + } +} + +func TestStoreAdd_InvalidConfig(t *testing.T) { + resetStoreMap() + var testStoreSpec = configv1beta1.NamespacedStoreSpec{ + Name: orasName, + Parameters: runtime.RawExtension{ + Raw: []byte("test"), + }, + } + + if err := storeAddOrReplace(testStoreSpec, orasName, testNamespace); err == nil { + t.Fatalf("storeAddOrReplace() expected error, actual %v", err) + } + if len(controllers.NamespacedStores.GetStores(testNamespace)) != 0 { + t.Fatalf("Store map expected size 0, actual %v", len(controllers.NamespacedStores.GetStores(testNamespace))) + } +} + +func TestStoreAdd_WithParameters(t *testing.T) { + resetStoreMap() + if len(controllers.NamespacedStores.GetStores(testNamespace)) != 0 { + t.Fatalf("Store map expected size 0, actual %v", len(controllers.NamespacedStores.GetStores(testNamespace))) + } + dirPath, err := test.CreatePlugin(sampleName) + if err != nil { + t.Fatalf("createPlugin() expected no error, actual %v", err) + } + defer os.RemoveAll(dirPath) + + var testStoreSpec = getOrasStoreSpec(sampleName, dirPath) + + if err := storeAddOrReplace(testStoreSpec, "testObject", testNamespace); err != nil { + t.Fatalf("storeAddOrReplace() expected no error, actual %v", err) + } + if len(controllers.NamespacedStores.GetStores(testNamespace)) != 1 { + t.Fatalf("Store map expected size 1, actual %v", len(controllers.NamespacedStores.GetStores(testNamespace))) + } +} + +func TestStoreAddOrReplace_PluginNotFound(t *testing.T) { + resetStoreMap() + var resource = "invalidplugin" + expectedMsg := "plugin not found" + var spec = getInvalidStoreSpec() + err := storeAddOrReplace(spec, resource, testNamespace) + + 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() + dirPath, err := test.CreatePlugin(sampleName) + if err != nil { + t.Fatalf("createPlugin() expected no error, actual %v", err) + } + defer os.RemoveAll(dirPath) + + var testStoreSpec = getOrasStoreSpec(sampleName, dirPath) + // add a Store + if err := storeAddOrReplace(testStoreSpec, sampleName, testNamespace); err != nil { + t.Fatalf("storeAddOrReplace() expected no error, actual %v", err) + } + if len(controllers.NamespacedStores.GetStores(testNamespace)) != 1 { + t.Fatalf("Store map expected size 1, actual %v", len(controllers.NamespacedStores.GetStores(testNamespace))) + } + + // modify the Store + var updatedSpec = configv1beta1.NamespacedStoreSpec{ + Name: sampleName, + Address: dirPath, + } + + if err := storeAddOrReplace(updatedSpec, sampleName, testNamespace); err != nil { + t.Fatalf("storeAddOrReplace() expected no error, actual %v", err) + } + + // validate no Store has been added + if len(controllers.NamespacedStores.GetStores(testNamespace)) != 1 { + t.Fatalf("Store map should be 1 after replacement, actual %v", len(controllers.NamespacedStores.GetStores(testNamespace))) + } + + controllers.NamespacedStores.DeleteStore(testNamespace, sampleName) + + if len(controllers.NamespacedStores.GetStores(testNamespace)) != 0 { + t.Fatalf("Store map should be 0 after deletion, actual %v", len(controllers.NamespacedStores.GetStores(testNamespace))) + } +} + +func TestStoreReconcile(t *testing.T) { + dirPath, err := test.CreatePlugin(orasName) + if err != nil { + t.Fatalf("createPlugin() expected no error, actual %v", err) + } + defer os.RemoveAll(dirPath) + + tests := []struct { + name string + store *configv1beta1.NamespacedStore + req *reconcile.Request + expectedErr bool + expectedStoreCount int + }{ + { + name: "nonexistent store", + req: &reconcile.Request{ + NamespacedName: types.NamespacedName{Name: "nonexistent"}, + }, + store: &configv1beta1.NamespacedStore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: storeName, + }, + Spec: configv1beta1.NamespacedStoreSpec{ + Name: orasName, + }, + }, + expectedErr: false, + expectedStoreCount: 0, + }, + { + name: "valid spec", + store: &configv1beta1.NamespacedStore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: storeName, + }, + Spec: configv1beta1.NamespacedStoreSpec{ + Name: orasName, + Address: dirPath, + }, + }, + expectedErr: false, + expectedStoreCount: 1, + }, + { + name: "invalid parameters", + store: &configv1beta1.NamespacedStore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: storeName, + }, + Spec: configv1beta1.NamespacedStoreSpec{ + Parameters: runtime.RawExtension{ + Raw: []byte("test"), + }, + }, + }, + expectedErr: true, + expectedStoreCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetStoreMap() + scheme, _ := test.CreateScheme() + client := fake.NewClientBuilder().WithScheme(scheme) + client.WithObjects(tt.store) + r := &StoreReconciler{ + Scheme: scheme, + Client: client.Build(), + } + var req reconcile.Request + if tt.req != nil { + req = *tt.req + } else { + req = reconcile.Request{ + NamespacedName: test.KeyFor(tt.store), + } + } + + _, err := r.Reconcile(context.Background(), req) + if tt.expectedErr != (err != nil) { + t.Fatalf("Reconcile() expected error %v, actual %v", tt.expectedErr, err) + } + if len(controllers.NamespacedStores.GetStores(testNamespace)) != tt.expectedStoreCount { + t.Fatalf("Store map expected size %v, actual %v", tt.expectedStoreCount, len(controllers.NamespacedStores.GetStores(testNamespace))) + } + }) + } +} + +func resetStoreMap() { + controllers.NamespacedStores = referrerstores.NewActiveStores() +} + +func getOrasStoreSpec(pluginName, pluginPath string) configv1beta1.NamespacedStoreSpec { + var parametersString = "{\"authProvider\":{\"name\":\"k8Secrets\",\"secrets\":[{\"secretName\":\"myregistrykey\"}]},\"cosignEnabled\":false,\"useHttp\":false}" + var storeParameters = []byte(parametersString) + + return configv1beta1.NamespacedStoreSpec{ + Name: pluginName, + Address: pluginPath, + Parameters: runtime.RawExtension{ + Raw: storeParameters, + }, + } +} + +func getInvalidStoreSpec() configv1beta1.NamespacedStoreSpec { + return configv1beta1.NamespacedStoreSpec{ + Name: "pluginnotfound", + Address: "test/path", + } +} diff --git a/pkg/controllers/utils/store.go b/pkg/controllers/utils/store.go new file mode 100644 index 0000000000..2ff0d9f03d --- /dev/null +++ b/pkg/controllers/utils/store.go @@ -0,0 +1,68 @@ +/* +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 utils + +import ( + "encoding/json" + "fmt" + + configv1beta1 "github.com/deislabs/ratify/api/v1beta1" + "github.com/deislabs/ratify/config" + "github.com/deislabs/ratify/pkg/controllers" + rc "github.com/deislabs/ratify/pkg/referrerstore/config" + sf "github.com/deislabs/ratify/pkg/referrerstore/factory" + "github.com/deislabs/ratify/pkg/verifier/types" + "github.com/sirupsen/logrus" +) + +func UpsertStoreMap(version, address, fullname, namespace string, storeConfig rc.StorePluginConfig) error { + // if the default version is not suitable, the plugin configuration should specify the desired version + if len(version) == 0 { + version = config.GetDefaultPluginVersion() + logrus.Infof("Version was empty, setting to default version: %v", version) + } + + if address == "" { + address = config.GetDefaultPluginPath() + logrus.Infof("Address was empty, setting to default path %v", address) + } + storeReference, err := sf.CreateStoreFromConfig(storeConfig, version, []string{address}) + + if err != nil || storeReference == nil { + logrus.Error(err, "store factory failed to create store from store config") + return fmt.Errorf("store factory failed to create store from store config, err: %w", err) + } + controllers.NamespacedStores.AddStore(namespace, fullname, storeReference) + logrus.Infof("store '%v' added to store map in namespace: %s", storeReference.Name(), namespace) + + return nil +} + +// Returns a store reference from spec +func CreateStoreConfig(raw []byte, name string, source *configv1beta1.PluginSource) (rc.StorePluginConfig, error) { + storeConfig := rc.StorePluginConfig{} + + if string(raw) != "" { + if err := json.Unmarshal(raw, &storeConfig); err != nil { + logrus.Error(err, "unable to decode store parameters", "Parameters.Raw", raw) + return rc.StorePluginConfig{}, err + } + } + storeConfig[types.Name] = name + if source != nil { + storeConfig[types.Source] = source + } + + return storeConfig, nil +} diff --git a/pkg/controllers/utils/store_test.go b/pkg/controllers/utils/store_test.go new file mode 100644 index 0000000000..970cf9744b --- /dev/null +++ b/pkg/controllers/utils/store_test.go @@ -0,0 +1,105 @@ +/* +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 utils + +import ( + "os" + "testing" + + configv1beta1 "github.com/deislabs/ratify/api/v1beta1" + rc "github.com/deislabs/ratify/pkg/referrerstore/config" + test "github.com/deislabs/ratify/pkg/utils" + "github.com/deislabs/ratify/pkg/verifier/types" +) + +const ( + storeName = "storeName" + testNamespace = "testNamespace" +) + +func TestUpsertStoreMap(t *testing.T) { + dirPath, err := test.CreatePlugin(storeName) + if err != nil { + t.Fatalf("createPlugin() expected no error, actual %v", err) + } + defer os.RemoveAll(dirPath) + + tests := []struct { + name string + address string + storeConfig rc.StorePluginConfig + expectedErr bool + }{ + { + name: "empty config", + storeConfig: rc.StorePluginConfig{}, + expectedErr: true, + }, + { + name: "valid config", + address: dirPath, + storeConfig: rc.StorePluginConfig{ + "name": storeName, + }, + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := UpsertStoreMap("", tt.address, storeName, testNamespace, tt.storeConfig) + if tt.expectedErr != (err != nil) { + t.Fatalf("expected error: %v, got: %v", tt.expectedErr, err) + } + }) + } +} + +func TestCreateStoreConfig(t *testing.T) { + tests := []struct { + name string + raw []byte + source *configv1beta1.PluginSource + expectedErr bool + expectedStoreName string + }{ + { + name: "invalid raw", + raw: []byte("invalid\n"), + expectedErr: true, + }, + { + name: "valid raw", + raw: []byte("{\"name\": \"storeName\"}"), + source: &configv1beta1.PluginSource{}, + expectedErr: false, + expectedStoreName: storeName, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := CreateStoreConfig(tt.raw, storeName, tt.source) + if tt.expectedErr != (err != nil) { + t.Fatalf("expected error: %v, got: %v", tt.expectedErr, err) + } + if _, ok := config[types.Name]; !ok { + config[types.Name] = "" + } + if config[types.Name] != tt.expectedStoreName { + t.Fatalf("expected store name: %s, got: %s", tt.expectedStoreName, config[types.Name]) + } + }) + } +} diff --git a/pkg/controllers/verifier_controller_test.go b/pkg/controllers/verifier_controller_test.go index 105b67aeca..9b5deabc2f 100644 --- a/pkg/controllers/verifier_controller_test.go +++ b/pkg/controllers/verifier_controller_test.go @@ -31,6 +31,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +const sampleName = "sample" + type mockResourceWriter struct { updateFailed bool } diff --git a/pkg/customresources/referrerstores/api.go b/pkg/customresources/referrerstores/api.go index 6b435c7e9c..63fcc47ae1 100644 --- a/pkg/customresources/referrerstores/api.go +++ b/pkg/customresources/referrerstores/api.go @@ -29,10 +29,4 @@ type ReferrerStoreManager interface { // DeleteStore deletes the policy from the given scope. DeleteStore(scope, storeName string) - - // IsEmpty returns true if there are no stores. - IsEmpty() bool - - // GetStoreCount returns the number of stores in all scopes. - GetStoreCount() int } diff --git a/pkg/customresources/referrerstores/stores.go b/pkg/customresources/referrerstores/stores.go index f60f83b0e4..fc1e67f563 100644 --- a/pkg/customresources/referrerstores/stores.go +++ b/pkg/customresources/referrerstores/stores.go @@ -16,12 +16,14 @@ limitations under the License. package referrerstores import ( + "sync" + + "github.com/deislabs/ratify/internal/constants" "github.com/deislabs/ratify/pkg/referrerstore" ) // ActiveStores implements the ReferrerStoreManager interface. type ActiveStores struct { - // TODO: Implement concurrent safety using sync.Map // The structure of the map is as follows: // The first level maps from scope to stores // The second level maps from store name to store @@ -33,57 +35,43 @@ type ActiveStores struct { // } // } // Note: Scope is utilized for organizing and isolating stores. In a Kubernetes (K8s) environment, the scope can be either a namespace or an empty string ("") for cluster-wide stores. - ScopedStores map[string]map[string]referrerstore.ReferrerStore + ScopedStores sync.Map } func NewActiveStores() ReferrerStoreManager { - return &ActiveStores{ - ScopedStores: make(map[string]map[string]referrerstore.ReferrerStore), - } + return &ActiveStores{} } // GetStores fulfills the ReferrerStoreManager interface. // It returns all the stores in the ActiveStores for the given scope. If no stores are found for the given scope, it returns cluster-wide stores. -// TODO: Current implementation fetches stores for all namespaces including cluster-wide ones. Will support actual namespace based stores in future. -func (s *ActiveStores) GetStores(_ string) []referrerstore.ReferrerStore { +func (s *ActiveStores) GetStores(scope string) []referrerstore.ReferrerStore { stores := []referrerstore.ReferrerStore{} - for _, scopedStores := range s.ScopedStores { - for _, store := range scopedStores { + if scopedStore, ok := s.ScopedStores.Load(scope); ok { + for _, store := range scopedStore.(map[string]referrerstore.ReferrerStore) { stores = append(stores, store) } } + if len(stores) == 0 && scope != constants.EmptyNamespace { + if clusterStore, ok := s.ScopedStores.Load(constants.EmptyNamespace); ok { + for _, store := range clusterStore.(map[string]referrerstore.ReferrerStore) { + stores = append(stores, store) + } + } + } return stores } // AddStore fulfills the ReferrerStoreManager interface. // It adds the given store under the given scope. func (s *ActiveStores) AddStore(scope, storeName string, store referrerstore.ReferrerStore) { - if _, ok := s.ScopedStores[scope]; !ok { - s.ScopedStores[scope] = make(map[string]referrerstore.ReferrerStore) - } - s.ScopedStores[scope][storeName] = store + scopedStore, _ := s.ScopedStores.LoadOrStore(scope, make(map[string]referrerstore.ReferrerStore)) + scopedStore.(map[string]referrerstore.ReferrerStore)[storeName] = store } // DeleteStore fulfills the ReferrerStoreManager interface. // It deletes the store with the given name under the given scope. func (s *ActiveStores) DeleteStore(scope, storeName string) { - if stores, ok := s.ScopedStores[scope]; ok { - delete(stores, storeName) - } -} - -// IsEmpty fulfills the ReferrerStoreManager interface. -// It returns true if there are no stores in the ActiveStores. -func (s *ActiveStores) IsEmpty() bool { - return s.GetStoreCount() == 0 -} - -// GetStore fulfills the ReferrerStoreManager interface. -// GetStoreCount returns the total number of stores in the ActiveStores. -func (s *ActiveStores) GetStoreCount() int { - count := 0 - for _, stores := range s.ScopedStores { - count += len(stores) + if scopedStore, ok := s.ScopedStores.Load(scope); ok { + delete(scopedStore.(map[string]referrerstore.ReferrerStore), storeName) } - return count } diff --git a/pkg/customresources/referrerstores/stores_test.go b/pkg/customresources/referrerstores/stores_test.go index 46a375801b..b590e8d093 100644 --- a/pkg/customresources/referrerstores/stores_test.go +++ b/pkg/customresources/referrerstores/stores_test.go @@ -74,13 +74,16 @@ func TestStoresOperations(t *testing.T) { stores.AddStore(namespace2, store1.Name(), store1) stores.AddStore(namespace2, store2.Name(), store2) - if stores.GetStoreCount() != 4 { - t.Fatalf("Expected 4 namespaces, got %d", stores.GetStoreCount()) + if len(stores.GetStores(namespace1)) != 2 { + t.Fatalf("Expected 2 stores in namespace %s, got %d", namespace1, len(stores.GetStores(namespace1))) + } + if len(stores.GetStores(namespace2)) != 2 { + t.Fatalf("Expected 2 stores in namespace %s, got %d", namespace2, len(stores.GetStores(namespace2))) } stores.DeleteStore(namespace2, store1.Name()) - if len(stores.GetStores(namespace2)) != 3 { - t.Fatalf("Expected 3 store in namespace %s, got %d", namespace2, len(stores.GetStores(namespace2))) + if len(stores.GetStores(namespace2)) != 1 { + t.Fatalf("Expected 1 store in namespace %s, got %d", namespace2, len(stores.GetStores(namespace2))) } stores.DeleteStore(namespace2, store2.Name()) @@ -97,8 +100,4 @@ func TestStoresOperations(t *testing.T) { if len(stores.GetStores(namespace1)) != 0 { t.Fatalf("Expected 0 stores in namespace %s, got %d", namespace1, len(stores.GetStores(namespace1))) } - - if !stores.IsEmpty() { - t.Fatalf("Expected stores to be empty") - } } diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index 6cafbacd0e..5e372b099b 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -200,13 +200,20 @@ func StartManager(certRotatorReady chan struct{}, probeAddr string) { setupLog.Error(err, "unable to create controller", "controller", "Verifier") os.Exit(1) } - if err = (&controllers.StoreReconciler{ + if err = (&clusterresource.StoreReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Store") os.Exit(1) } + if err = (&namespaceresource.StoreReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Namespaced Store") + os.Exit(1) + } if err = (&controllers.CertificateStoreReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index d4a799c234..f1711ebf60 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -264,7 +264,7 @@ RATIFY_NAMESPACE=gatekeeper-system } # apply a invalid verifier CR, validate status with error - sed 's/:v1/:invalid/' ./config/samples/config_v1beta1_store_dynamic.yaml > invalidstore.yaml + sed 's/:v1/:invalid/' ./config/samples/clustered/store/config_v1beta1_store_dynamic.yaml > invalidstore.yaml run kubectl apply -f invalidstore.yaml assert_success # wait for download of image @@ -438,7 +438,7 @@ RATIFY_NAMESPACE=gatekeeper-system echo "cleaning up" wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo --namespace default --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo1 --namespace default --ignore-not-found=true' - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/config_v1beta1_store_oras_http.yaml' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/clustered/store/config_v1beta1_store_oras_http.yaml' } run kubectl apply -f ./library/default/template.yaml @@ -448,7 +448,7 @@ RATIFY_NAMESPACE=gatekeeper-system assert_success sleep 5 # apply store CRD with K8s secret auth provier enabled - run kubectl apply -f ./config/samples/config_v1beta1_store_oras_k8secretAuth.yaml + run kubectl apply -f ./config/samples/clustered/store/config_v1beta1_store_oras_k8secretAuth.yaml assert_success sleep 5 run kubectl run demo --namespace default --image=registry:5000/notation:signed diff --git a/test/bats/plugin-test.bats b/test/bats/plugin-test.bats index b395325c04..83c926df74 100644 --- a/test/bats/plugin-test.bats +++ b/test/bats/plugin-test.bats @@ -59,6 +59,44 @@ SLEEP_TIME=1 assert_success } +@test "cosign test" { + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-key --namespace default --force --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-unsigned --namespace default --force --ignore-not-found=true' + } + run kubectl apply -f ./library/default/template.yaml + assert_success + sleep 5 + run kubectl apply -f ./library/default/samples/constraint.yaml + assert_success + sleep 5 + + run kubectl run cosign-demo-key --namespace default --image=registry:5000/cosign:signed-key + assert_success + + run kubectl run cosign-demo-unsigned --namespace default --image=registry:5000/cosign:unsigned + assert_failure +} + +@test "cosign keyless test" { + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-keyless --namespace default --force --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/config_v1beta1_verifier_cosign.yaml' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/clustered/store/config_v1beta1_store_oras_http.yaml' + } + + # use imperative command to guarantee useHttp is updated + run kubectl replace -f ./config/samples/config_v1beta1_verifier_cosign_keyless.yaml + sleep 5 + + run kubectl replace -f ./config/samples/clustered/store/config_v1beta1_store_oras.yaml + sleep 5 + + wait_for_process 20 10 'kubectl run cosign-demo-keyless --namespace default --image=wabbitnetworks.azurecr.io/test/cosign-image:signed-keyless' +} + @test "licensechecker test" { teardown() { echo "cleaning up" @@ -103,7 +141,7 @@ SLEEP_TIME=1 sleep 5 run kubectl run sbom --namespace default --image=registry:5000/sbom:v0 assert_failure - + run kubectl apply -f ./config/samples/config_v1beta1_verifier_sbom.yaml # wait for the httpserver cache to be invalidated sleep 15