diff --git a/pkg/api/apis/operators/clusterserviceversion_types.go b/pkg/api/apis/operators/clusterserviceversion_types.go index 94398e72e6..4fb14dbabc 100644 --- a/pkg/api/apis/operators/clusterserviceversion_types.go +++ b/pkg/api/apis/operators/clusterserviceversion_types.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "sort" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" ) // ClusterServiceVersionKind is the PascalCase name of a CSV's kind. @@ -120,6 +122,88 @@ type CustomResourceDefinitions struct { Required []CRDDescription } +// WebhookAdmissionType is the type of admission webhooks supported by OLM +type WebhookAdmissionType string + +const ( + // InstallWebhookValidating is for validating admission webhooks + InstallWebhookValidating WebhookAdmissionType = "ValidatingAdmissionWebhook" + // InstallWebhookMutating is for mutating admission webhooks + InstallWebhookMutating WebhookAdmissionType = "MutatingAdmissionWebhook" +) + +// WebhookDescription provides details to OLM about required webhooks +// +k8s:openapi-gen=true +type WebhookDescription struct { + Name string `json:"name"` + Type WebhookAdmissionType `json:"type"` + DeploymentName string `json:"deploymentName,omitempty"` + ContainerPort int32 `json:"containerPort,omitempty"` + Rules []admissionregistrationv1.RuleWithOperations `json:"rules"` + FailurePolicy *admissionregistrationv1.FailurePolicyType `json:"failurePolicy,omitempty"` + MatchPolicy *admissionregistrationv1.MatchPolicyType `json:"matchPolicy,omitempty"` + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` + ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` + SideEffects *admissionregistrationv1.SideEffectClass `json:"sideEffects"` + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + AdmissionReviewVersions []string `json:"admissionReviewVersions"` + ReinvocationPolicy *admissionregistrationv1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` + WebhookPath *string `json:"webhookPath,omitempty"` +} + +// GetValidatingWebhook returns a ValidatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetValidatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.ValidatingWebhook { + return admissionregistrationv1.ValidatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + } +} + +// GetMutatingWebhook returns a MutatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetMutatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.MutatingWebhook { + return admissionregistrationv1.MutatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + ReinvocationPolicy: w.ReinvocationPolicy, + } +} + +// MorphDomainName returns the result of replacing all periods in the given Webhook name with hyphens +func (w *WebhookDescription) MorphDomainName() string { + // Replace all '.'s with "-"s to convert to a DNS-1035 label + return strings.Replace(w.Name, ".", "-", -1) +} + // APIServiceDefinitions declares all of the extension apis managed or required by // an operator being ran by ClusterServiceVersion. type APIServiceDefinitions struct { @@ -135,6 +219,7 @@ type ClusterServiceVersionSpec struct { Maturity string CustomResourceDefinitions CustomResourceDefinitions APIServiceDefinitions APIServiceDefinitions + WebhookDefinitions []WebhookDescription NativeAPIs []metav1.GroupVersionKind MinKubeVersion string DisplayName string diff --git a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go index 03638d4060..b2210d2459 100644 --- a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go +++ b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "sort" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" ) const ( @@ -108,6 +110,88 @@ type APIServiceDescription struct { ActionDescriptor []ActionDescriptor `json:"actionDescriptors,omitempty"` } +// WebhookAdmissionType is the type of admission webhooks supported by OLM +type WebhookAdmissionType string + +const ( + // InstallWebhookValidating is for validating admission webhooks + InstallWebhookValidating WebhookAdmissionType = "ValidatingAdmissionWebhook" + // InstallWebhookMutating is for mutating admission webhooks + InstallWebhookMutating WebhookAdmissionType = "MutatingAdmissionWebhook" +) + +// WebhookDescription provides details to OLM about required webhooks +// +k8s:openapi-gen=true +type WebhookDescription struct { + Name string `json:"name"` + Type WebhookAdmissionType `json:"type"` + DeploymentName string `json:"deploymentName,omitempty"` + ContainerPort int32 `json:"containerPort,omitempty"` + Rules []admissionregistrationv1.RuleWithOperations `json:"rules"` + FailurePolicy *admissionregistrationv1.FailurePolicyType `json:"failurePolicy,omitempty"` + MatchPolicy *admissionregistrationv1.MatchPolicyType `json:"matchPolicy,omitempty"` + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` + ObjectSelector *metav1.LabelSelector `json:"objectSelector,omitempty"` + SideEffects *admissionregistrationv1.SideEffectClass `json:"sideEffects"` + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + AdmissionReviewVersions []string `json:"admissionReviewVersions"` + ReinvocationPolicy *admissionregistrationv1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` + WebhookPath *string `json:"webhookPath,omitempty"` +} + +// GetValidatingWebhook returns a ValidatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetValidatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.ValidatingWebhook { + return admissionregistrationv1.ValidatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + } +} + +// GetMutatingWebhook returns a MutatingWebhook generated from the WebhookDescription +func (w *WebhookDescription) GetMutatingWebhook(namespace string, caBundle []byte) admissionregistrationv1.MutatingWebhook { + return admissionregistrationv1.MutatingWebhook{ + Name: w.Name, + Rules: w.Rules, + FailurePolicy: w.FailurePolicy, + MatchPolicy: w.MatchPolicy, + NamespaceSelector: w.NamespaceSelector, + ObjectSelector: w.ObjectSelector, + SideEffects: w.SideEffects, + TimeoutSeconds: w.TimeoutSeconds, + AdmissionReviewVersions: w.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: w.MorphDomainName() + "-svc", + Namespace: namespace, + Path: w.WebhookPath, + }, + CABundle: caBundle, + }, + ReinvocationPolicy: w.ReinvocationPolicy, + } +} + +// MorphDomainName returns the result of replacing all periods in the given Webhook name with hyphens +func (w *WebhookDescription) MorphDomainName() string { + // Replace all '.'s with "-"s to convert to a DNS-1035 label + return strings.Replace(w.Name, ".", "-", -1) +} + // APIResourceReference is a Kubernetes resource type used by a custom resource // +k8s:openapi-gen=true type APIResourceReference struct { @@ -147,6 +231,7 @@ type ClusterServiceVersionSpec struct { Maturity string `json:"maturity,omitempty"` CustomResourceDefinitions CustomResourceDefinitions `json:"customresourcedefinitions,omitempty"` APIServiceDefinitions APIServiceDefinitions `json:"apiservicedefinitions,omitempty"` + WebhookDefinitions []WebhookDescription `json:"webhookdefinitions,omitempty"` NativeAPIs []metav1.GroupVersionKind `json:"nativeAPIs,omitempty"` MinKubeVersion string `json:"minKubeVersion,omitempty"` DisplayName string `json:"displayName"` diff --git a/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go b/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go index 208bd436b4..70b4ea0248 100644 --- a/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go +++ b/pkg/api/apis/operators/v1alpha1/zz_generated.conversion.go @@ -25,6 +25,7 @@ import ( unsafe "unsafe" operators "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" @@ -459,6 +460,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*WebhookDescription)(nil), (*operators.WebhookDescription)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(a.(*WebhookDescription), b.(*operators.WebhookDescription), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*operators.WebhookDescription)(nil), (*WebhookDescription)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(a.(*operators.WebhookDescription), b.(*WebhookDescription), scope) + }); err != nil { + return err + } return nil } @@ -852,6 +863,7 @@ func autoConvert_v1alpha1_ClusterServiceVersionSpec_To_operators_ClusterServiceV if err := Convert_v1alpha1_APIServiceDefinitions_To_operators_APIServiceDefinitions(&in.APIServiceDefinitions, &out.APIServiceDefinitions, s); err != nil { return err } + out.WebhookDefinitions = *(*[]operators.WebhookDescription)(unsafe.Pointer(&in.WebhookDefinitions)) out.NativeAPIs = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.NativeAPIs)) out.MinKubeVersion = in.MinKubeVersion out.DisplayName = in.DisplayName @@ -888,6 +900,7 @@ func autoConvert_operators_ClusterServiceVersionSpec_To_v1alpha1_ClusterServiceV if err := Convert_operators_APIServiceDefinitions_To_v1alpha1_APIServiceDefinitions(&in.APIServiceDefinitions, &out.APIServiceDefinitions, s); err != nil { return err } + out.WebhookDefinitions = *(*[]WebhookDescription)(unsafe.Pointer(&in.WebhookDefinitions)) out.NativeAPIs = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.NativeAPIs)) out.MinKubeVersion = in.MinKubeVersion out.DisplayName = in.DisplayName @@ -1693,3 +1706,49 @@ func autoConvert_operators_SubscriptionStatus_To_v1alpha1_SubscriptionStatus(in func Convert_operators_SubscriptionStatus_To_v1alpha1_SubscriptionStatus(in *operators.SubscriptionStatus, out *SubscriptionStatus, s conversion.Scope) error { return autoConvert_operators_SubscriptionStatus_To_v1alpha1_SubscriptionStatus(in, out, s) } + +func autoConvert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(in *WebhookDescription, out *operators.WebhookDescription, s conversion.Scope) error { + out.Name = in.Name + out.Type = operators.WebhookAdmissionType(in.Type) + out.DeploymentName = in.DeploymentName + out.ContainerPort = in.ContainerPort + out.Rules = *(*[]admissionregistrationv1.RuleWithOperations)(unsafe.Pointer(&in.Rules)) + out.FailurePolicy = (*admissionregistrationv1.FailurePolicyType)(unsafe.Pointer(in.FailurePolicy)) + out.MatchPolicy = (*admissionregistrationv1.MatchPolicyType)(unsafe.Pointer(in.MatchPolicy)) + out.NamespaceSelector = (*v1.LabelSelector)(unsafe.Pointer(in.NamespaceSelector)) + out.ObjectSelector = (*v1.LabelSelector)(unsafe.Pointer(in.ObjectSelector)) + out.SideEffects = (*admissionregistrationv1.SideEffectClass)(unsafe.Pointer(in.SideEffects)) + out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) + out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.ReinvocationPolicy = (*admissionregistrationv1.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.WebhookPath = (*string)(unsafe.Pointer(in.WebhookPath)) + return nil +} + +// Convert_v1alpha1_WebhookDescription_To_operators_WebhookDescription is an autogenerated conversion function. +func Convert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(in *WebhookDescription, out *operators.WebhookDescription, s conversion.Scope) error { + return autoConvert_v1alpha1_WebhookDescription_To_operators_WebhookDescription(in, out, s) +} + +func autoConvert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(in *operators.WebhookDescription, out *WebhookDescription, s conversion.Scope) error { + out.Name = in.Name + out.Type = WebhookAdmissionType(in.Type) + out.DeploymentName = in.DeploymentName + out.ContainerPort = in.ContainerPort + out.Rules = *(*[]admissionregistrationv1.RuleWithOperations)(unsafe.Pointer(&in.Rules)) + out.FailurePolicy = (*admissionregistrationv1.FailurePolicyType)(unsafe.Pointer(in.FailurePolicy)) + out.MatchPolicy = (*admissionregistrationv1.MatchPolicyType)(unsafe.Pointer(in.MatchPolicy)) + out.NamespaceSelector = (*v1.LabelSelector)(unsafe.Pointer(in.NamespaceSelector)) + out.ObjectSelector = (*v1.LabelSelector)(unsafe.Pointer(in.ObjectSelector)) + out.SideEffects = (*admissionregistrationv1.SideEffectClass)(unsafe.Pointer(in.SideEffects)) + out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) + out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.ReinvocationPolicy = (*admissionregistrationv1.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.WebhookPath = (*string)(unsafe.Pointer(in.WebhookPath)) + return nil +} + +// Convert_operators_WebhookDescription_To_v1alpha1_WebhookDescription is an autogenerated conversion function. +func Convert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(in *operators.WebhookDescription, out *WebhookDescription, s conversion.Scope) error { + return autoConvert_operators_WebhookDescription_To_v1alpha1_WebhookDescription(in, out, s) +} diff --git a/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go b/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go index b950b42f10..92f17d5d43 100644 --- a/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/api/apis/operators/v1alpha1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1alpha1 import ( json "encoding/json" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -404,6 +405,13 @@ func (in *ClusterServiceVersionSpec) DeepCopyInto(out *ClusterServiceVersionSpec in.Version.DeepCopyInto(&out.Version) in.CustomResourceDefinitions.DeepCopyInto(&out.CustomResourceDefinitions) in.APIServiceDefinitions.DeepCopyInto(&out.APIServiceDefinitions) + if in.WebhookDefinitions != nil { + in, out := &in.WebhookDefinitions, &out.WebhookDefinitions + *out = make([]WebhookDescription, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.NativeAPIs != nil { in, out := &in.NativeAPIs, &out.NativeAPIs *out = make([]v1.GroupVersionKind, len(*in)) @@ -1198,3 +1206,71 @@ func (in *SubscriptionStatus) DeepCopy() *SubscriptionStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookDescription) DeepCopyInto(out *WebhookDescription) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]admissionregistrationv1.RuleWithOperations, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FailurePolicy != nil { + in, out := &in.FailurePolicy, &out.FailurePolicy + *out = new(admissionregistrationv1.FailurePolicyType) + **out = **in + } + if in.MatchPolicy != nil { + in, out := &in.MatchPolicy, &out.MatchPolicy + *out = new(admissionregistrationv1.MatchPolicyType) + **out = **in + } + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.ObjectSelector != nil { + in, out := &in.ObjectSelector, &out.ObjectSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.SideEffects != nil { + in, out := &in.SideEffects, &out.SideEffects + *out = new(admissionregistrationv1.SideEffectClass) + **out = **in + } + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.AdmissionReviewVersions != nil { + in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ReinvocationPolicy != nil { + in, out := &in.ReinvocationPolicy, &out.ReinvocationPolicy + *out = new(admissionregistrationv1.ReinvocationPolicyType) + **out = **in + } + if in.WebhookPath != nil { + in, out := &in.WebhookPath, &out.WebhookPath + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookDescription. +func (in *WebhookDescription) DeepCopy() *WebhookDescription { + if in == nil { + return nil + } + out := new(WebhookDescription) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/apis/operators/zz_generated.deepcopy.go b/pkg/api/apis/operators/zz_generated.deepcopy.go index 52ebe586b9..f8d251c4ed 100644 --- a/pkg/api/apis/operators/zz_generated.deepcopy.go +++ b/pkg/api/apis/operators/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package operators import ( json "encoding/json" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -404,6 +405,13 @@ func (in *ClusterServiceVersionSpec) DeepCopyInto(out *ClusterServiceVersionSpec in.Version.DeepCopyInto(&out.Version) in.CustomResourceDefinitions.DeepCopyInto(&out.CustomResourceDefinitions) in.APIServiceDefinitions.DeepCopyInto(&out.APIServiceDefinitions) + if in.WebhookDefinitions != nil { + in, out := &in.WebhookDefinitions, &out.WebhookDefinitions + *out = make([]WebhookDescription, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.NativeAPIs != nil { in, out := &in.NativeAPIs, &out.NativeAPIs *out = make([]v1.GroupVersionKind, len(*in)) @@ -1312,3 +1320,71 @@ func (in *SubscriptionStatus) DeepCopy() *SubscriptionStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookDescription) DeepCopyInto(out *WebhookDescription) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]admissionregistrationv1.RuleWithOperations, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FailurePolicy != nil { + in, out := &in.FailurePolicy, &out.FailurePolicy + *out = new(admissionregistrationv1.FailurePolicyType) + **out = **in + } + if in.MatchPolicy != nil { + in, out := &in.MatchPolicy, &out.MatchPolicy + *out = new(admissionregistrationv1.MatchPolicyType) + **out = **in + } + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.ObjectSelector != nil { + in, out := &in.ObjectSelector, &out.ObjectSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.SideEffects != nil { + in, out := &in.SideEffects, &out.SideEffects + *out = new(admissionregistrationv1.SideEffectClass) + **out = **in + } + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.AdmissionReviewVersions != nil { + in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ReinvocationPolicy != nil { + in, out := &in.ReinvocationPolicy, &out.ReinvocationPolicy + *out = new(admissionregistrationv1.ReinvocationPolicyType) + **out = **in + } + if in.WebhookPath != nil { + in, out := &in.WebhookPath, &out.WebhookPath + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookDescription. +func (in *WebhookDescription) DeepCopy() *WebhookDescription { + if in == nil { + return nil + } + out := new(WebhookDescription) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index d1c0b43dab..e5a041a8b1 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -1344,6 +1344,13 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v return } + // Install required Webhooks and update strategy with serving cert data + strategy, syncError = a.installWebhookRequirements(out, strategy) + if syncError != nil { + out.SetPhaseWithEvent(v1alpha1.CSVPhaseFailed, v1alpha1.CSVReasonComponentFailed, fmt.Sprintf("install webhook failed: %s", syncError), now, a.recorder) + return + } + if syncError = installer.Install(strategy); syncError != nil { if install.IsErrorUnrecoverable(syncError) { logger.Infof("Setting CSV reason to failed without retry: %v", syncError) diff --git a/pkg/controller/operators/olm/webhooks.go b/pkg/controller/operators/olm/webhooks.go new file mode 100644 index 0000000000..e41cb3de0b --- /dev/null +++ b/pkg/controller/operators/olm/webhooks.go @@ -0,0 +1,415 @@ +// TODO: Refactor this code with the API Cert code. +package olm + +import ( + "fmt" + "time" + + log "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/certs" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" +) + +func (a *Operator) installWebhookRequirements(csv *v1alpha1.ClusterServiceVersion, strategy install.Strategy) (install.Strategy, error) { + logger := log.WithFields(log.Fields{ + "csv": csv.GetName(), + "namespace": csv.GetNamespace(), + }) + + // Assume the strategy is for a deployment + strategyDetailsDeployment, ok := strategy.(*install.StrategyDetailsDeployment) + if !ok { + return nil, fmt.Errorf("unsupported InstallStrategy type") + } + + // Return early if there are no WebhookDefinitions + webhookDescriptions := csv.Spec.WebhookDefinitions + if len(webhookDescriptions) == 0 { + return strategyDetailsDeployment, nil + } + + // Create the CA + expiration := time.Now().Add(DefaultCertValidFor) + ca, err := certs.GenerateCA(expiration, Organization) + if err != nil { + logger.Debug("failed to generate CA") + return nil, err + } + rotateAt := expiration.Add(-1 * DefaultCertMinFresh) + + depSpecs := make(map[string]appsv1.DeploymentSpec) + for _, sddSpec := range strategyDetailsDeployment.DeploymentSpecs { + depSpecs[sddSpec.Name] = sddSpec.Spec + } + + // Create all resources required, and update the matching DeploymentSpec's Volume and VolumeMounts + // Get List of Webhooks + for _, desc := range webhookDescriptions { + depSpec, ok := depSpecs[desc.DeploymentName] + if !ok { + return nil, fmt.Errorf("StrategyDetailsDeployment missing deployment %s for webhook", desc.DeploymentName) + } + + newDepSpec, err := a.installWebhook(desc, ca, rotateAt, depSpec, csv) + if err != nil { + return nil, err + } + depSpecs[desc.DeploymentName] = *newDepSpec + } + + // Replace all matching DeploymentSpecs in the strategy + for i, sddSpec := range strategyDetailsDeployment.DeploymentSpecs { + if depSpec, ok := depSpecs[sddSpec.Name]; ok { + strategyDetailsDeployment.DeploymentSpecs[i].Spec = depSpec + } + } + + // Set CSV cert status + csv.Status.CertsLastUpdated = metav1.Now() + csv.Status.CertsRotateAt = metav1.NewTime(rotateAt) + + return strategyDetailsDeployment, nil +} + +func (a *Operator) installWebhook(desc v1alpha1.WebhookDescription, ca *certs.KeyPair, rotateAt time.Time, depSpec appsv1.DeploymentSpec, csv *v1alpha1.ClusterServiceVersion) (*appsv1.DeploymentSpec, error) { + logger := log.WithFields(log.Fields{ + "csv": csv.GetName(), + "namespace": csv.GetNamespace(), + "webhook": desc.Name, + }) + + // Create a service for the deployment + containerPort := 443 + if desc.ContainerPort > 0 { + containerPort = int(desc.ContainerPort) + } + service := &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: int32(443), + TargetPort: intstr.FromInt(containerPort), + }, + }, + Selector: depSpec.Selector.MatchLabels, + }, + } + service.SetName(desc.MorphDomainName() + "-svc") + service.SetNamespace(csv.GetNamespace()) + ownerutil.AddNonBlockingOwner(service, csv) + + existingService, err := a.lister.CoreV1().ServiceLister().Services(csv.GetNamespace()).Get(service.GetName()) + if err == nil { + if !ownerutil.Adoptable(csv, existingService.GetOwnerReferences()) { + return nil, fmt.Errorf("service %s not safe to replace: extraneous ownerreferences found", service.GetName()) + } + service.SetOwnerReferences(append(service.GetOwnerReferences(), existingService.GetOwnerReferences()...)) + + // Delete the Service to replace + deleteErr := a.opClient.DeleteService(service.GetNamespace(), service.GetName(), &metav1.DeleteOptions{}) + if err != nil && !k8serrors.IsNotFound(deleteErr) { + return nil, fmt.Errorf("could not delete existing service %s", service.GetName()) + } + } + + // Attempt to create the Service + _, err = a.opClient.CreateService(service) + if err != nil { + logger.Warnf("could not create service %s", service.GetName()) + return nil, fmt.Errorf("could not create service %s: %s", service.GetName(), err.Error()) + } + + // Create signed serving cert + hosts := []string{ + fmt.Sprintf("%s.%s", service.GetName(), csv.GetNamespace()), + fmt.Sprintf("%s.%s.svc", service.GetName(), csv.GetNamespace()), + } + servingPair, err := certs.CreateSignedServingPair(rotateAt, Organization, ca, hosts) + if err != nil { + logger.Warnf("could not generate signed certs for hosts %v", hosts) + return nil, err + } + + // Create Secret for serving cert + certPEM, privPEM, err := servingPair.ToPEM() + if err != nil { + logger.Warnf("unable to convert serving certificate and private key to PEM format for Webhook %s", desc.Name) + return nil, err + } + + secret := &corev1.Secret{ + Data: map[string][]byte{ + "tls.crt": certPEM, + "tls.key": privPEM, + }, + Type: corev1.SecretTypeTLS, + } + secret.SetName(desc.MorphDomainName() + "-cert") + secret.SetNamespace(csv.GetNamespace()) + + // Add olmcasha hash as a label to the + caPEM, _, err := ca.ToPEM() + if err != nil { + logger.Warnf("unable to convert CA certificate to PEM format for Webhook %s", desc.Name) + return nil, err + } + caHash := certs.PEMSHA256(caPEM) + secret.SetAnnotations(map[string]string{OLMCAHashAnnotationKey: caHash}) + + existingSecret, err := a.lister.CoreV1().SecretLister().Secrets(csv.GetNamespace()).Get(secret.GetName()) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingSecret.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(secret, csv) + } + + // Attempt an update + if _, err := a.opClient.UpdateSecret(secret); err != nil { + logger.Warnf("could not update secret %s", secret.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + // Create the secret + ownerutil.AddNonBlockingOwner(secret, csv) + _, err = a.opClient.CreateSecret(secret) + if err != nil { + log.Warnf("could not create secret %s", secret.GetName()) + return nil, err + } + } else { + return nil, err + } + + // create Role and RoleBinding to allow the deployment to mount the Secret + secretRole := &rbacv1.Role{ + Rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"secrets"}, + ResourceNames: []string{secret.GetName()}, + }, + }, + } + secretRole.SetName(secret.GetName()) + secretRole.SetNamespace(csv.GetNamespace()) + + existingSecretRole, err := a.lister.RbacV1().RoleLister().Roles(csv.GetNamespace()).Get(secretRole.GetName()) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingSecretRole.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(secretRole, csv) + } + + // Attempt an update + if _, err := a.opClient.UpdateRole(secretRole); err != nil { + logger.Warnf("could not update secret role %s", secretRole.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + // Create the role + ownerutil.AddNonBlockingOwner(secretRole, csv) + _, err = a.opClient.CreateRole(secretRole) + if err != nil { + log.Warnf("could not create secret role %s", secretRole.GetName()) + return nil, err + } + } else { + return nil, err + } + + if depSpec.Template.Spec.ServiceAccountName == "" { + depSpec.Template.Spec.ServiceAccountName = "default" + } + + secretRoleBinding := &rbacv1.RoleBinding{ + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: depSpec.Template.Spec.ServiceAccountName, + Namespace: csv.GetNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: secretRole.GetName(), + }, + } + secretRoleBinding.SetName(secret.GetName()) + secretRoleBinding.SetNamespace(csv.GetNamespace()) + + existingSecretRoleBinding, err := a.lister.RbacV1().RoleBindingLister().RoleBindings(csv.GetNamespace()).Get(secretRoleBinding.GetName()) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingSecretRoleBinding.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(secretRoleBinding, csv) + } + + // Attempt an update + if _, err := a.opClient.UpdateRoleBinding(secretRoleBinding); err != nil { + logger.Warnf("could not update secret rolebinding %s", secretRoleBinding.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + // Create the role + ownerutil.AddNonBlockingOwner(secretRoleBinding, csv) + _, err = a.opClient.CreateRoleBinding(secretRoleBinding) + if err != nil { + log.Warnf("could not create secret rolebinding with dep spec: %+v", depSpec) + return nil, err + } + } else { + return nil, err + } + + // Update deployment with secret volume mount. + volume := corev1.Volume{ + Name: "webhook-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secret.GetName(), + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "webhook.crt", + }, + { + Key: "tls.key", + Path: "webhook.key", + }, + }, + }, + }, + } + + replaced := false + for i, v := range depSpec.Template.Spec.Volumes { + if v.Name == volume.Name { + depSpec.Template.Spec.Volumes[i] = volume + replaced = true + break + } + } + if !replaced { + depSpec.Template.Spec.Volumes = append(depSpec.Template.Spec.Volumes, volume) + } + + mount := corev1.VolumeMount{ + Name: volume.Name, + MountPath: "/webhook.local.config/certificates", + } + for i, container := range depSpec.Template.Spec.Containers { + found := false + for j, m := range container.VolumeMounts { + if m.Name == mount.Name { + found = true + break + } + + // Replace if mounting to the same location. + if m.MountPath == mount.MountPath { + container.VolumeMounts[j] = mount + found = true + break + } + } + if !found { + container.VolumeMounts = append(container.VolumeMounts, mount) + } + + depSpec.Template.Spec.Containers[i] = container + } + + // Setting the olm hash label forces a rollout and ensures that the new secret + // is used by the webhook if not hot reloading. + depSpec.Template.ObjectMeta.SetAnnotations(map[string]string{OLMCAHashAnnotationKey: caHash}) + + switch desc.Type { + case v1alpha1.InstallWebhookValidating: + webhooks := []admissionregistrationv1.ValidatingWebhook{ + desc.GetValidatingWebhook(csv.GetNamespace(), caPEM), + } + existingHook, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(desc.Name, metav1.GetOptions{}) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingHook.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(existingHook, csv) + } + + // Update the list of webhooks + existingHook.Webhooks = webhooks + + // Attempt an update + if _, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(existingHook); err != nil { + logger.Warnf("could not update ValidatingWebhookConfiguration %s", existingHook.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + // Create a ValidatingWebhookConfiguration + hook := admissionregistrationv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: desc.Name, + Namespace: csv.GetNamespace(), + }, + Webhooks: webhooks, + } + + // Add an owner + ownerutil.AddNonBlockingOwner(&hook, csv) + if _, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(&hook); err != nil { + log.Errorf("Webhooks: Error create creating ValidationVebhookConfiguration: %v", err) + return nil, err + } + } else { + return nil, err + } + case v1alpha1.InstallWebhookMutating: + webhooks := []admissionregistrationv1.MutatingWebhook{ + desc.GetMutatingWebhook(csv.GetNamespace(), caPEM), + } + existingHook, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().Get(desc.Name, metav1.GetOptions{}) + if err == nil { + // Check if the only owners are this CSV or in this CSV's replacement chain + if ownerutil.Adoptable(csv, existingHook.GetOwnerReferences()) { + ownerutil.AddNonBlockingOwner(existingHook, csv) + } + + // Update the list of webhooks + existingHook.Webhooks = webhooks + + // Attempt an update + if _, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().Update(existingHook); err != nil { + logger.Warnf("could not update MutatingWebhookConfiguration %s", existingHook.GetName()) + return nil, err + } + } else if k8serrors.IsNotFound(err) { + hook := admissionregistrationv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: desc.Name, + Namespace: csv.GetNamespace(), + }, + Webhooks: webhooks, + } + // Add an owner + ownerutil.AddNonBlockingOwner(&hook, csv) + if _, err := a.opClient.KubernetesInterface().AdmissionregistrationV1().MutatingWebhookConfigurations().Create(&hook); err != nil { + log.Errorf("Webhooks: Error creating mutating MutatingVebhookConfiguration: %v", err) + return nil, err + } + } else { + return nil, err + } + } + + return &depSpec, nil +} diff --git a/pkg/package-server/apis/openapi/zz_generated.openapi.go b/pkg/package-server/apis/openapi/zz_generated.openapi.go index 0824dd9654..37d2800a28 100644 --- a/pkg/package-server/apis/openapi/zz_generated.openapi.go +++ b/pkg/package-server/apis/openapi/zz_generated.openapi.go @@ -40,6 +40,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.InstallMode": schema_api_apis_operators_v1alpha1_InstallMode(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.SpecDescriptor": schema_api_apis_operators_v1alpha1_SpecDescriptor(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.StatusDescriptor": schema_api_apis_operators_v1alpha1_StatusDescriptor(ref), + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1.WebhookDescription": schema_api_apis_operators_v1alpha1_WebhookDescription(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version.OperatorVersion": schema_operator_lifecycle_manager_pkg_lib_version_OperatorVersion(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/apps/v1alpha1.AppLink": schema_package_server_apis_apps_v1alpha1_AppLink(ref), "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/apps/v1alpha1.CSVDescription": schema_package_server_apis_apps_v1alpha1_CSVDescription(ref), @@ -606,6 +607,117 @@ func schema_api_apis_operators_v1alpha1_StatusDescriptor(ref common.ReferenceCal } } +func schema_api_apis_operators_v1alpha1_WebhookDescription(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "WebhookDescription provides details to OLM about required webhooks", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "deploymentName": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "containerPort": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int32", + }, + }, + "rules": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/api/admissionregistration/v1.RuleWithOperations"), + }, + }, + }, + }, + }, + "failurePolicy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "matchPolicy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "namespaceSelector": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"), + }, + }, + "objectSelector": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"), + }, + }, + "sideEffects": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "timeoutSeconds": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int32", + }, + }, + "admissionReviewVersions": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "reinvocationPolicy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "webhookPath": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "type", "rules", "sideEffects", "admissionReviewVersions"}, + }, + }, + Dependencies: []string{ + "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + } +} + func schema_operator_lifecycle_manager_pkg_lib_version_OperatorVersion(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/test/e2e/csv_e2e_test.go b/test/e2e/csv_e2e_test.go index ce145206b0..503a1bf50f 100644 --- a/test/e2e/csv_e2e_test.go +++ b/test/e2e/csv_e2e_test.go @@ -20,6 +20,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" v1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" @@ -1364,6 +1365,186 @@ func TestCreateCSVWithOwnedAPIService(t *testing.T) { require.NoError(t, err) } + +func TestCreateCSVWithWebhook(t *testing.T) { + defer cleaner.NotifyTestComplete(t, true) + + c := newKubeClient(t) + crc := newCRClient(t) + + depName := genName("hat-server") + mockGroup := fmt.Sprintf("hats.%s.redhat.com", genName("")) + version := "v1alpha1" + mockGroupVersion := strings.Join([]string{mockGroup, version}, "/") + mockKinds := []string{"fez", "fedora"} + depSpec := newMockExtServerDeployment(depName, mockGroupVersion, mockKinds) + apiServiceName := strings.Join([]string{version, mockGroup}, ".") + + // Create CSV for the package-server + strategy := install.StrategyDetailsDeployment{ + DeploymentSpecs: []install.StrategyDeploymentSpec{ + { + Name: depName, + Spec: depSpec, + }, + }, + } + strategyRaw, err := json.Marshal(strategy) + sideEffects := admissionregistrationv1.SideEffectClassNone + failurePolicy := admissionregistrationv1.Ignore + webhooks := []v1alpha1.WebhookDescription{ + v1alpha1.WebhookDescription{ + Name: apiServiceName, + Type: v1alpha1.InstallWebhookValidating, + DeploymentName: depName, + ContainerPort: 443, + NamespaceSelector: &metav1.LabelSelector{}, + SideEffects: &sideEffects, + FailurePolicy: &failurePolicy, + AdmissionReviewVersions: []string{"v1beta1"}, + Rules: []admissionregistrationv1.RuleWithOperations{}, + }, + } + + csv := v1alpha1.ClusterServiceVersion{ + Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0.0", + InstallModes: []v1alpha1.InstallMode{ + { + Type: v1alpha1.InstallModeTypeOwnNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeSingleNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeMultiNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeAllNamespaces, + Supported: true, + }, + }, + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategyName: install.InstallStrategyNameDeployment, + StrategySpecRaw: strategyRaw, + }, + WebhookDefinitions: webhooks, + }, + } + csv.SetName(depName) + + // Create the APIService CSV + cleanupCSV, err := createCSV(t, c, crc, csv, testNamespace, false, false) + require.NoError(t, err) + defer func() { + watcher, err := c.KubernetesInterface().AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Watch(metav1.ListOptions{FieldSelector: "metadata.name=" + apiServiceName}) + require.NoError(t, err) + + deleted := make(chan struct{}) + go func() { + events := watcher.ResultChan() + for { + select { + case evt := <-events: + if evt.Type == watch.Deleted { + deleted <- struct{}{} + return + } + case <-time.After(pollDuration): + require.FailNow(t, "apiservice not cleaned up after CSV deleted") + } + } + }() + + cleanupCSV() + <-deleted + }() + + fetchedCSV, err := fetchCSV(t, crc, csv.Name, testNamespace, csvSucceededChecker) + require.NoError(t, err) + + // Should create Deployment + dep, err := c.GetDeployment(testNamespace, depName) + require.NoError(t, err, "error getting expected Deployment") + + // Should create Webhook + _, err = c.KubernetesInterface().AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Get(apiServiceName, metav1.GetOptions{}) + require.NoError(t, err, "error getting expected APIService") + + // Should create Service + _, err = c.GetService(testNamespace, olm.APIServiceNameToServiceName(apiServiceName)+"-svc") + require.NoError(t, err, "error getting expected ServiapiServicece") + + // Should create certificate Secret + secretName := fmt.Sprintf("%s-cert", olm.APIServiceNameToServiceName(apiServiceName)) + _, err = c.GetSecret(testNamespace, secretName) + require.NoError(t, err, "error getting expected Secret") + + // Should create a Role for the Secret + _, err = c.GetRole(testNamespace, secretName) + require.NoError(t, err, "error getting expected Secret Role") + + // Should create a RoleBinding for the Secret + _, err = c.GetRoleBinding(testNamespace, secretName) + require.NoError(t, err, "error getting exptected Secret RoleBinding") + + // Store the ca sha annotation + oldCAAnnotation, ok := dep.Spec.Template.GetAnnotations()[olm.OLMCAHashAnnotationKey] + require.True(t, ok, "expected olm sha annotation not present on existing pod template") + + // Induce a cert rotation + fetchedCSV.Status.CertsLastUpdated = metav1.Now() + fetchedCSV.Status.CertsRotateAt = metav1.Now() + fetchedCSV, err = crc.OperatorsV1alpha1().ClusterServiceVersions(testNamespace).UpdateStatus(fetchedCSV) + require.NoError(t, err) + + _, err = fetchCSV(t, crc, csv.Name, testNamespace, func(csv *v1alpha1.ClusterServiceVersion) bool { + // Should create deployment + dep, err = c.GetDeployment(testNamespace, depName) + require.NoError(t, err) + + // Should have a new ca hash annotation + newCAAnnotation, ok := dep.Spec.Template.GetAnnotations()[olm.OLMCAHashAnnotationKey] + require.True(t, ok, "expected olm sha annotation not present in new pod template") + + if newCAAnnotation != oldCAAnnotation { + // Check for success + return csvSucceededChecker(csv) + } + + return false + }) + require.NoError(t, err, "failed to rotate cert") +/* + // Get the APIService UID + oldWebhookUID := webhook.GetUID() + + // Delete the Webhook + err = c.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(apiServiceName, &metav1.DeleteOptions{}) + require.NoError(t, err) + + // Wait for CSV success + fetchedCSV, err = fetchCSV(t, crc, csv.GetName(), testNamespace, func(csv *v1alpha1.ClusterServiceVersion) bool { + // Should create an APIService + webhook, err := c.KubernetesInterface().AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Get(apiServiceName, metav1.GetOptions{}) + if err != nil { + require.True(t, k8serrors.IsNotFound(err)) + return false + } + + if csvSucceededChecker(csv) { + require.NotEqual(t, oldWebhookUID, webhook.GetUID()) + return true + } + + return false + }) + require.NoError(t, err)*/ +} + func TestUpdateCSVWithOwnedAPIService(t *testing.T) { defer cleaner.NotifyTestComplete(t, true)