mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
Implements Webhook Generator (#3121)
* adding webhook generators Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> * bumping bundle Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> * linting Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> * fixing copy-paste error Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> * common webhook functions Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> * removing duplicates. Adding tests for generator Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> * docs Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> --------- Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
This commit is contained in:
parent
2ca08fbfb6
commit
1cf8f68276
16 changed files with 1445 additions and 286 deletions
130
apis/generators/v1alpha1/generator_webhook.go
Normal file
130
apis/generators/v1alpha1/generator_webhook.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebhookSpec controls the behavior of the external generator. Any body parameters should be passed to the server through the parameters field.
|
||||||
|
type WebhookSpec struct {
|
||||||
|
// Webhook Method
|
||||||
|
// +optional, default GET
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
|
||||||
|
// Webhook url to call
|
||||||
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
// +optional
|
||||||
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
|
|
||||||
|
// Body
|
||||||
|
// +optional
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
// +optional
|
||||||
|
Timeout *metav1.Duration `json:"timeout,omitempty"`
|
||||||
|
|
||||||
|
// Result formatting
|
||||||
|
Result WebhookResult `json:"result"`
|
||||||
|
|
||||||
|
// Secrets to fill in templates
|
||||||
|
// These secrets will be passed to the templating function as key value pairs under the given name
|
||||||
|
// +optional
|
||||||
|
Secrets []WebhookSecret `json:"secrets,omitempty"`
|
||||||
|
|
||||||
|
// PEM encoded CA bundle used to validate webhook server certificate. Only used
|
||||||
|
// if the Server URL is using HTTPS protocol. This parameter is ignored for
|
||||||
|
// plain HTTP protocol connection. If not set the system root certificates
|
||||||
|
// are used to validate the TLS connection.
|
||||||
|
// +optional
|
||||||
|
CABundle []byte `json:"caBundle,omitempty"`
|
||||||
|
|
||||||
|
// The provider for the CA bundle to use to validate webhook server certificate.
|
||||||
|
// +optional
|
||||||
|
CAProvider *WebhookCAProvider `json:"caProvider,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookCAProviderType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WebhookCAProviderTypeSecret WebhookCAProviderType = "Secret"
|
||||||
|
WebhookCAProviderTypeConfigMap WebhookCAProviderType = "ConfigMap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines a location to fetch the cert for the webhook provider from.
|
||||||
|
type WebhookCAProvider struct {
|
||||||
|
// The type of provider to use such as "Secret", or "ConfigMap".
|
||||||
|
// +kubebuilder:validation:Enum="Secret";"ConfigMap"
|
||||||
|
Type WebhookCAProviderType `json:"type"`
|
||||||
|
|
||||||
|
// The name of the object located at the provider type.
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// The key the value inside of the provider type to use, only used with "Secret" type
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
|
||||||
|
// The namespace the Provider type is in.
|
||||||
|
// +optional
|
||||||
|
Namespace *string `json:"namespace,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookResult struct {
|
||||||
|
// Json path of return value
|
||||||
|
// +optional
|
||||||
|
JSONPath string `json:"jsonPath,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookSecret struct {
|
||||||
|
// Name of this secret in templates
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Secret ref to fill in credentials
|
||||||
|
SecretRef SecretKeySelector `json:"secretRef"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecretKeySelector struct {
|
||||||
|
// The name of the Secret resource being referred to.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
// The key where the token is found.
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook connects to a third party API server to handle the secrets generation
|
||||||
|
// configuration parameters in spec.
|
||||||
|
// You can specify the server, the token, and additional body parameters.
|
||||||
|
// See documentation for the full API specification for requests and responses.
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:storageversion
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:resource:scope=Namespaced,categories={webhook},shortName=webhookl
|
||||||
|
type Webhook struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec WebhookSpec `json:"spec,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// ExternalList contains a list of Webhook Generator resources.
|
||||||
|
type WebhookList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []Webhook `json:"items"`
|
||||||
|
}
|
|
@ -68,6 +68,14 @@ var (
|
||||||
PasswordGroupVersionKind = SchemeGroupVersion.WithKind(PasswordKind)
|
PasswordGroupVersionKind = SchemeGroupVersion.WithKind(PasswordKind)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Webhook type metadata.
|
||||||
|
var (
|
||||||
|
WebhookKind = reflect.TypeOf(Webhook{}).Name()
|
||||||
|
WebhookGroupKind = schema.GroupKind{Group: Group, Kind: WebhookKind}.String()
|
||||||
|
WebhookKindAPIVersion = WebhookKind + "." + SchemeGroupVersion.String()
|
||||||
|
WebhookGroupVersionKind = SchemeGroupVersion.WithKind(WebhookKind)
|
||||||
|
)
|
||||||
|
|
||||||
// Fake type metadata.
|
// Fake type metadata.
|
||||||
var (
|
var (
|
||||||
FakeKind = reflect.TypeOf(Fake{}).Name()
|
FakeKind = reflect.TypeOf(Fake{}).Name()
|
||||||
|
@ -91,4 +99,5 @@ func init() {
|
||||||
SchemeBuilder.Register(&Fake{}, &FakeList{})
|
SchemeBuilder.Register(&Fake{}, &FakeList{})
|
||||||
SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
|
SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
|
||||||
SchemeBuilder.Register(&Password{}, &PasswordList{})
|
SchemeBuilder.Register(&Password{}, &PasswordList{})
|
||||||
|
SchemeBuilder.Register(&Webhook{}, &WebhookList{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
"github.com/external-secrets/external-secrets/apis/meta/v1"
|
"github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -653,6 +654,21 @@ func (in *PasswordSpec) DeepCopy() *PasswordSpec {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
|
||||||
|
func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(SecretKeySelector)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *VaultDynamicSecret) DeepCopyInto(out *VaultDynamicSecret) {
|
func (in *VaultDynamicSecret) DeepCopyInto(out *VaultDynamicSecret) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -735,3 +751,155 @@ func (in *VaultDynamicSecretSpec) DeepCopy() *VaultDynamicSecretSpec {
|
||||||
in.DeepCopyInto(out)
|
in.DeepCopyInto(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Webhook) DeepCopyInto(out *Webhook) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Webhook.
|
||||||
|
func (in *Webhook) DeepCopy() *Webhook {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Webhook)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *Webhook) 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 *WebhookCAProvider) DeepCopyInto(out *WebhookCAProvider) {
|
||||||
|
*out = *in
|
||||||
|
if in.Namespace != nil {
|
||||||
|
in, out := &in.Namespace, &out.Namespace
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookCAProvider.
|
||||||
|
func (in *WebhookCAProvider) DeepCopy() *WebhookCAProvider {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(WebhookCAProvider)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *WebhookList) DeepCopyInto(out *WebhookList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]Webhook, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookList.
|
||||||
|
func (in *WebhookList) DeepCopy() *WebhookList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(WebhookList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *WebhookList) 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 *WebhookResult) DeepCopyInto(out *WebhookResult) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookResult.
|
||||||
|
func (in *WebhookResult) DeepCopy() *WebhookResult {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(WebhookResult)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *WebhookSecret) DeepCopyInto(out *WebhookSecret) {
|
||||||
|
*out = *in
|
||||||
|
out.SecretRef = in.SecretRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSecret.
|
||||||
|
func (in *WebhookSecret) DeepCopy() *WebhookSecret {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(WebhookSecret)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *WebhookSpec) DeepCopyInto(out *WebhookSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Headers != nil {
|
||||||
|
in, out := &in.Headers, &out.Headers
|
||||||
|
*out = make(map[string]string, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
(*out)[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.Timeout != nil {
|
||||||
|
in, out := &in.Timeout, &out.Timeout
|
||||||
|
*out = new(metav1.Duration)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
out.Result = in.Result
|
||||||
|
if in.Secrets != nil {
|
||||||
|
in, out := &in.Secrets, &out.Secrets
|
||||||
|
*out = make([]WebhookSecret, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.CABundle != nil {
|
||||||
|
in, out := &in.CABundle, &out.CABundle
|
||||||
|
*out = make([]byte, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.CAProvider != nil {
|
||||||
|
in, out := &in.CAProvider, &out.CAProvider
|
||||||
|
*out = new(WebhookCAProvider)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSpec.
|
||||||
|
func (in *WebhookSpec) DeepCopy() *WebhookSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(WebhookSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
141
config/crds/bases/generators.external-secrets.io_webhooks.yaml
Normal file
141
config/crds/bases/generators.external-secrets.io_webhooks.yaml
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.14.0
|
||||||
|
name: webhooks.generators.external-secrets.io
|
||||||
|
spec:
|
||||||
|
group: generators.external-secrets.io
|
||||||
|
names:
|
||||||
|
categories:
|
||||||
|
- webhook
|
||||||
|
kind: Webhook
|
||||||
|
listKind: WebhookList
|
||||||
|
plural: webhooks
|
||||||
|
shortNames:
|
||||||
|
- webhookl
|
||||||
|
singular: webhook
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: |-
|
||||||
|
Webhook connects to a third party API server to handle the secrets generation
|
||||||
|
configuration parameters in spec.
|
||||||
|
You can specify the server, the token, and additional body parameters.
|
||||||
|
See documentation for the full API specification for requests and responses.
|
||||||
|
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: WebhookSpec controls the behavior of the external generator.
|
||||||
|
Any body parameters should be passed to the server through the parameters
|
||||||
|
field.
|
||||||
|
properties:
|
||||||
|
body:
|
||||||
|
description: Body
|
||||||
|
type: string
|
||||||
|
caBundle:
|
||||||
|
description: |-
|
||||||
|
PEM encoded CA bundle used to validate webhook server certificate. Only used
|
||||||
|
if the Server URL is using HTTPS protocol. This parameter is ignored for
|
||||||
|
plain HTTP protocol connection. If not set the system root certificates
|
||||||
|
are used to validate the TLS connection.
|
||||||
|
format: byte
|
||||||
|
type: string
|
||||||
|
caProvider:
|
||||||
|
description: The provider for the CA bundle to use to validate webhook
|
||||||
|
server certificate.
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: The key the value inside of the provider type to
|
||||||
|
use, only used with "Secret" type
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: The name of the object located at the provider type.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: The namespace the Provider type is in.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: The type of provider to use such as "Secret", or
|
||||||
|
"ConfigMap".
|
||||||
|
enum:
|
||||||
|
- Secret
|
||||||
|
- ConfigMap
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
headers:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Headers
|
||||||
|
type: object
|
||||||
|
method:
|
||||||
|
description: Webhook Method
|
||||||
|
type: string
|
||||||
|
result:
|
||||||
|
description: Result formatting
|
||||||
|
properties:
|
||||||
|
jsonPath:
|
||||||
|
description: Json path of return value
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
secrets:
|
||||||
|
description: |-
|
||||||
|
Secrets to fill in templates
|
||||||
|
These secrets will be passed to the templating function as key value pairs under the given name
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of this secret in templates
|
||||||
|
type: string
|
||||||
|
secretRef:
|
||||||
|
description: Secret ref to fill in credentials
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: The key where the token is found.
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: The name of the Secret resource being referred
|
||||||
|
to.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- secretRef
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
timeout:
|
||||||
|
description: Timeout
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
description: Webhook url to call
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- result
|
||||||
|
- url
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
|
@ -10910,3 +10910,149 @@ spec:
|
||||||
name: kubernetes
|
name: kubernetes
|
||||||
namespace: default
|
namespace: default
|
||||||
path: /convert
|
path: /convert
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.14.0
|
||||||
|
name: webhooks.generators.external-secrets.io
|
||||||
|
spec:
|
||||||
|
group: generators.external-secrets.io
|
||||||
|
names:
|
||||||
|
categories:
|
||||||
|
- webhook
|
||||||
|
kind: Webhook
|
||||||
|
listKind: WebhookList
|
||||||
|
plural: webhooks
|
||||||
|
shortNames:
|
||||||
|
- webhookl
|
||||||
|
singular: webhook
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: |-
|
||||||
|
Webhook connects to a third party API server to handle the secrets generation
|
||||||
|
configuration parameters in spec.
|
||||||
|
You can specify the server, the token, and additional body parameters.
|
||||||
|
See documentation for the full API specification for requests and responses.
|
||||||
|
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: WebhookSpec controls the behavior of the external generator. Any body parameters should be passed to the server through the parameters field.
|
||||||
|
properties:
|
||||||
|
body:
|
||||||
|
description: Body
|
||||||
|
type: string
|
||||||
|
caBundle:
|
||||||
|
description: |-
|
||||||
|
PEM encoded CA bundle used to validate webhook server certificate. Only used
|
||||||
|
if the Server URL is using HTTPS protocol. This parameter is ignored for
|
||||||
|
plain HTTP protocol connection. If not set the system root certificates
|
||||||
|
are used to validate the TLS connection.
|
||||||
|
format: byte
|
||||||
|
type: string
|
||||||
|
caProvider:
|
||||||
|
description: The provider for the CA bundle to use to validate webhook server certificate.
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: The key the value inside of the provider type to use, only used with "Secret" type
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: The name of the object located at the provider type.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: The namespace the Provider type is in.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: The type of provider to use such as "Secret", or "ConfigMap".
|
||||||
|
enum:
|
||||||
|
- Secret
|
||||||
|
- ConfigMap
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
headers:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Headers
|
||||||
|
type: object
|
||||||
|
method:
|
||||||
|
description: Webhook Method
|
||||||
|
type: string
|
||||||
|
result:
|
||||||
|
description: Result formatting
|
||||||
|
properties:
|
||||||
|
jsonPath:
|
||||||
|
description: Json path of return value
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
secrets:
|
||||||
|
description: |-
|
||||||
|
Secrets to fill in templates
|
||||||
|
These secrets will be passed to the templating function as key value pairs under the given name
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of this secret in templates
|
||||||
|
type: string
|
||||||
|
secretRef:
|
||||||
|
description: Secret ref to fill in credentials
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: The key where the token is found.
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: The name of the Secret resource being referred to.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- secretRef
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
timeout:
|
||||||
|
description: Timeout
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
description: Webhook url to call
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- result
|
||||||
|
- url
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
|
conversion:
|
||||||
|
strategy: Webhook
|
||||||
|
webhook:
|
||||||
|
conversionReviewVersions:
|
||||||
|
- v1
|
||||||
|
clientConfig:
|
||||||
|
service:
|
||||||
|
name: kubernetes
|
||||||
|
namespace: default
|
||||||
|
path: /convert
|
||||||
|
|
21
docs/api/generator/webhook.md
Normal file
21
docs/api/generator/webhook.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The Webhook generator is very similar to SecretStore generator, and provides a way to use external systems to generate sensitive information.
|
||||||
|
|
||||||
|
## Output Keys and Values
|
||||||
|
|
||||||
|
Webhook calls are expected to produce valid JSON objects. All keys within that JSON object will be exported as keys to the kubernetes Secret.
|
||||||
|
|
||||||
|
## Example Manifest
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{% include 'generator-webhook.yaml' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example `ExternalSecret` that references the Webhook generator using an internal `Secret`:
|
||||||
|
```yaml
|
||||||
|
{% include 'generator-webhook-example.yaml' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate a kubernetes secret with the following values:
|
||||||
|
```yaml
|
||||||
|
parameter: test
|
||||||
|
```
|
|
@ -123,3 +123,5 @@ spec:
|
||||||
key: <key inside secret>
|
key: <key inside secret>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Webhook as generators
|
||||||
|
You can also leverage webhooks as generators, following the same syntax. The only difference is that the webhook generator needs its source secrets to be labeled, as opposed to webhook secretstores. Please see the [generator-webhook](../api/generator/webhook.md) documentation for more information.
|
14
docs/snippets/generator-webhook-example.yaml
Normal file
14
docs/snippets/generator-webhook-example.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: "webhook"
|
||||||
|
spec:
|
||||||
|
refreshInterval: "30m"
|
||||||
|
target:
|
||||||
|
name: webhook-secret
|
||||||
|
dataFrom:
|
||||||
|
- sourceRef:
|
||||||
|
generatorRef:
|
||||||
|
apiVersion: generators.external-secrets.io/v1alpha1
|
||||||
|
kind: Webhook
|
||||||
|
name: "webhook"
|
28
docs/snippets/generator-webhook.yaml
Normal file
28
docs/snippets/generator-webhook.yaml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% raw %}
|
||||||
|
apiVersion: generators.external-secrets.io/v1alpha1
|
||||||
|
kind: Webhook
|
||||||
|
metadata:
|
||||||
|
name: webhook
|
||||||
|
spec:
|
||||||
|
url: "http://httpbin.org/get?parameter={{ .auth.param }}"
|
||||||
|
result:
|
||||||
|
jsonPath: "$.args"
|
||||||
|
headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Basic {{ print .auth.username ":" .auth.password | b64enc }}
|
||||||
|
secrets:
|
||||||
|
- name: auth
|
||||||
|
secretRef:
|
||||||
|
name: webhook-credentials
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: webhook-credentials
|
||||||
|
labels:
|
||||||
|
generators.external-secrets.io/type: webhook #Needed to allow webhook to use this secret
|
||||||
|
data:
|
||||||
|
username: dGVzdA== # "test"
|
||||||
|
password: dGVzdA== # "test"
|
||||||
|
param: dGVzdA== # "test"
|
||||||
|
{% endraw %}
|
|
@ -59,6 +59,7 @@ nav:
|
||||||
- Vault Dynamic Secret: api/generator/vault.md
|
- Vault Dynamic Secret: api/generator/vault.md
|
||||||
- Password: api/generator/password.md
|
- Password: api/generator/password.md
|
||||||
- Fake: api/generator/fake.md
|
- Fake: api/generator/fake.md
|
||||||
|
- Webhook: api/generator/webhook.md
|
||||||
- Reference Docs:
|
- Reference Docs:
|
||||||
- API specification: api/spec.md
|
- API specification: api/spec.md
|
||||||
- Controller Options: api/controller-options.md
|
- Controller Options: api/controller-options.md
|
||||||
|
|
106
pkg/common/webhook/models.go
Normal file
106
pkg/common/webhook/models.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
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 webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Spec struct {
|
||||||
|
// Webhook Method
|
||||||
|
// +optional, default GET
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
|
||||||
|
// Webhook url to call
|
||||||
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
// +optional
|
||||||
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
|
|
||||||
|
// Body
|
||||||
|
// +optional
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
// +optional
|
||||||
|
Timeout *metav1.Duration `json:"timeout,omitempty"`
|
||||||
|
|
||||||
|
// Result formatting
|
||||||
|
Result Result `json:"result"`
|
||||||
|
|
||||||
|
// Secrets to fill in templates
|
||||||
|
// These secrets will be passed to the templating function as key value pairs under the given name
|
||||||
|
// +optional
|
||||||
|
Secrets []Secret `json:"secrets,omitempty"`
|
||||||
|
|
||||||
|
// PEM encoded CA bundle used to validate webhook server certificate. Only used
|
||||||
|
// if the Server URL is using HTTPS protocol. This parameter is ignored for
|
||||||
|
// plain HTTP protocol connection. If not set the system root certificates
|
||||||
|
// are used to validate the TLS connection.
|
||||||
|
// +optional
|
||||||
|
CABundle []byte `json:"caBundle,omitempty"`
|
||||||
|
|
||||||
|
// The provider for the CA bundle to use to validate webhook server certificate.
|
||||||
|
// +optional
|
||||||
|
CAProvider *CAProvider `json:"caProvider,omitempty"`
|
||||||
|
}
|
||||||
|
type CAProviderType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CAProviderTypeSecret CAProviderType = "Secret"
|
||||||
|
CAProviderTypeConfigMap CAProviderType = "ConfigMap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines a location to fetch the cert for the webhook provider from.
|
||||||
|
type CAProvider struct {
|
||||||
|
// The type of provider to use such as "Secret", or "ConfigMap".
|
||||||
|
// +kubebuilder:validation:Enum="Secret";"ConfigMap"
|
||||||
|
Type CAProviderType `json:"type"`
|
||||||
|
|
||||||
|
// The name of the object located at the provider type.
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// The key the value inside of the provider type to use, only used with "Secret" type
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
|
||||||
|
// The namespace the Provider type is in.
|
||||||
|
// +optional
|
||||||
|
Namespace *string `json:"namespace,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
// Json path of return value
|
||||||
|
// +optional
|
||||||
|
JSONPath string `json:"jsonPath,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Secret struct {
|
||||||
|
// Name of this secret in templates
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Secret ref to fill in credentials
|
||||||
|
SecretRef SecretKeySelector `json:"secretRef"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecretKeySelector struct {
|
||||||
|
// The name of the Secret resource being referred to.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
// The key where the token is found.
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
|
||||||
|
Namespace *string `json:"namespace,omitempty"`
|
||||||
|
}
|
318
pkg/common/webhook/webhook.go
Normal file
318
pkg/common/webhook/webhook.go
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
/*
|
||||||
|
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 webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
tpl "text/template"
|
||||||
|
|
||||||
|
"github.com/PaesslerAG/jsonpath"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
|
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/constants"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/metrics"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/template/v2"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Webhook struct {
|
||||||
|
Kube client.Client
|
||||||
|
Namespace string
|
||||||
|
StoreKind string
|
||||||
|
HTTP *http.Client
|
||||||
|
EnforceLabels bool
|
||||||
|
ClusterScoped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) getStoreSecret(ctx context.Context, ref SecretKeySelector) (*corev1.Secret, error) {
|
||||||
|
ke := client.ObjectKey{
|
||||||
|
Name: ref.Name,
|
||||||
|
Namespace: w.Namespace,
|
||||||
|
}
|
||||||
|
if w.ClusterScoped {
|
||||||
|
if ref.Namespace == nil {
|
||||||
|
return nil, fmt.Errorf("no namespace on ClusterScoped webhook secret %s", ref.Name)
|
||||||
|
}
|
||||||
|
ke.Namespace = *ref.Namespace
|
||||||
|
}
|
||||||
|
secret := &corev1.Secret{}
|
||||||
|
if err := w.Kube.Get(ctx, ke, secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
|
||||||
|
}
|
||||||
|
if w.EnforceLabels {
|
||||||
|
expected, ok := secret.Labels["generators.external-secrets.io/type"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("secret does not contain needed label to be used on webhook generator")
|
||||||
|
}
|
||||||
|
if expected != "webhook" {
|
||||||
|
return nil, fmt.Errorf("secret type is not 'webhook'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
func (w *Webhook) GetSecretMap(ctx context.Context, provider *Spec, ref *esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||||
|
result, err := w.GetWebhookData(ctx, provider, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// We always want json here, so just parse it out
|
||||||
|
jsondata := interface{}(nil)
|
||||||
|
if err := json.Unmarshal(result, &jsondata); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response json: %w", err)
|
||||||
|
}
|
||||||
|
// Get subdata via jsonpath, if given
|
||||||
|
if provider.Result.JSONPath != "" {
|
||||||
|
jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the value is a string, try to parse it as json
|
||||||
|
jsonstring, ok := jsondata.(string)
|
||||||
|
if ok {
|
||||||
|
// This could also happen if the response was a single json-encoded string
|
||||||
|
// but that is an extremely unlikely scenario
|
||||||
|
if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use the data as a key-value map
|
||||||
|
jsonvalue, ok := jsondata.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
|
||||||
|
}
|
||||||
|
// Change the map of generic objects to a map of byte arrays
|
||||||
|
values := make(map[string][]byte)
|
||||||
|
for rKey, rValue := range jsonvalue {
|
||||||
|
jVal, ok := rValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to get response (wrong type in key '%s': %T)", rKey, rValue)
|
||||||
|
}
|
||||||
|
values[rKey] = []byte(jVal)
|
||||||
|
}
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) GetTemplateData(ctx context.Context, ref *esv1beta1.ExternalSecretDataRemoteRef, secrets []Secret) (map[string]map[string]string, error) {
|
||||||
|
data := map[string]map[string]string{}
|
||||||
|
if ref != nil {
|
||||||
|
data["remoteRef"] = map[string]string{
|
||||||
|
"key": url.QueryEscape(ref.Key),
|
||||||
|
"version": url.QueryEscape(ref.Version),
|
||||||
|
"property": url.QueryEscape(ref.Property),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, secref := range secrets {
|
||||||
|
if _, ok := data[secref.Name]; !ok {
|
||||||
|
data[secref.Name] = make(map[string]string)
|
||||||
|
}
|
||||||
|
secret, err := w.getStoreSecret(ctx, secref.SecretRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for sKey, sVal := range secret.Data {
|
||||||
|
data[secref.Name][sKey] = string(sVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) GetWebhookData(ctx context.Context, provider *Spec, ref *esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||||
|
if w.HTTP == nil {
|
||||||
|
return nil, fmt.Errorf("http client not initialized")
|
||||||
|
}
|
||||||
|
data, err := w.GetTemplateData(ctx, ref, provider.Secrets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
method := provider.Method
|
||||||
|
if method == "" {
|
||||||
|
method = http.MethodGet
|
||||||
|
}
|
||||||
|
url, err := ExecuteTemplateString(provider.URL, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse url: %w", err)
|
||||||
|
}
|
||||||
|
body, err := ExecuteTemplate(provider.Body, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
for hKey, hValueTpl := range provider.Headers {
|
||||||
|
hValue, err := ExecuteTemplateString(hValueTpl, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
|
||||||
|
}
|
||||||
|
req.Header.Add(hKey, hValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := w.HTTP.Do(req)
|
||||||
|
metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to call endpoint: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == 404 {
|
||||||
|
return nil, esv1beta1.NoSecretError{}
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
|
||||||
|
}
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) GetHTTPClient(provider *Spec) (*http.Client, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
if provider.Timeout != nil {
|
||||||
|
client.Timeout = provider.Timeout.Duration
|
||||||
|
}
|
||||||
|
if len(provider.CABundle) == 0 && provider.CAProvider == nil {
|
||||||
|
// No need to process ca stuff if it is not there
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
caCertPool, err := w.GetCACertPool(provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf := &tls.Config{
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
client.Transport = &http.Transport{TLSClientConfig: tlsConf}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) GetCACertPool(provider *Spec) (*x509.CertPool, error) {
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
if len(provider.CABundle) > 0 {
|
||||||
|
ok := caCertPool.AppendCertsFromPEM(provider.CABundle)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to append cabundle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider.CAProvider != nil {
|
||||||
|
var cert []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch provider.CAProvider.Type {
|
||||||
|
case CAProviderTypeSecret:
|
||||||
|
cert, err = w.GetCertFromSecret(provider)
|
||||||
|
case CAProviderTypeConfigMap:
|
||||||
|
cert, err = w.GetCertFromConfigMap(provider)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown caprovider type: %s", provider.CAProvider.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := caCertPool.AppendCertsFromPEM(cert)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to append cabundle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caCertPool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) GetCertFromSecret(provider *Spec) ([]byte, error) {
|
||||||
|
secretRef := esmeta.SecretKeySelector{
|
||||||
|
Name: provider.CAProvider.Name,
|
||||||
|
Namespace: &w.Namespace,
|
||||||
|
Key: provider.CAProvider.Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider.CAProvider.Namespace != nil {
|
||||||
|
secretRef.Namespace = provider.CAProvider.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
cert, err := resolvers.SecretKeyRef(
|
||||||
|
ctx,
|
||||||
|
w.Kube,
|
||||||
|
w.StoreKind,
|
||||||
|
w.Namespace,
|
||||||
|
&secretRef,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(cert), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) GetCertFromConfigMap(provider *Spec) ([]byte, error) {
|
||||||
|
objKey := client.ObjectKey{
|
||||||
|
Name: provider.CAProvider.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider.CAProvider.Namespace != nil {
|
||||||
|
objKey.Namespace = *provider.CAProvider.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
configMapRef := &corev1.ConfigMap{}
|
||||||
|
ctx := context.Background()
|
||||||
|
err := w.Kube.Get(ctx, objKey, configMapRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get caprovider secret %s: %w", objKey.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := configMapRef.Data[provider.CAProvider.Key]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to get caprovider configmap %s -> %s", objKey.Name, provider.CAProvider.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
|
||||||
|
result, err := ExecuteTemplate(tmpl, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return result.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
|
||||||
|
var result bytes.Buffer
|
||||||
|
if tmpl == "" {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if err := urlt.Execute(&result, data); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -24,4 +24,5 @@ import (
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/generator/gcr"
|
_ "github.com/external-secrets/external-secrets/pkg/generator/gcr"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/generator/password"
|
_ "github.com/external-secrets/external-secrets/pkg/generator/password"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/generator/vault"
|
_ "github.com/external-secrets/external-secrets/pkg/generator/vault"
|
||||||
|
_ "github.com/external-secrets/external-secrets/pkg/generator/webhook"
|
||||||
)
|
)
|
||||||
|
|
72
pkg/generator/webhook/webhook.go
Normal file
72
pkg/generator/webhook/webhook.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
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 webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/common/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Webhook struct {
|
||||||
|
wh webhook.Webhook
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webhook) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kclient client.Client, ns string) (map[string][]byte, error) {
|
||||||
|
w.wh.EnforceLabels = true
|
||||||
|
w.wh.ClusterScoped = false
|
||||||
|
provider, err := parseSpec(jsonSpec.Raw)
|
||||||
|
w.wh = webhook.Webhook{}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.wh.Namespace = ns
|
||||||
|
w.url = provider.URL
|
||||||
|
w.wh.Kube = kclient
|
||||||
|
w.wh.HTTP, err = w.wh.GetHTTPClient(provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get store: %w", err)
|
||||||
|
}
|
||||||
|
return w.wh.GetSecretMap(ctx, provider, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSpec(data []byte) (*webhook.Spec, error) {
|
||||||
|
var spec genv1alpha1.Webhook
|
||||||
|
err := json.Unmarshal(data, &spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := webhook.Spec{}
|
||||||
|
d, err := json.Marshal(spec.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(d, &out)
|
||||||
|
return &out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
genv1alpha1.Register(genv1alpha1.WebhookKind, &Webhook{})
|
||||||
|
}
|
266
pkg/generator/webhook/webhook_test.go
Normal file
266
pkg/generator/webhook/webhook_test.go
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
/*
|
||||||
|
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 webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
|
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
Case string `json:"case,omitempty"`
|
||||||
|
Args args `json:"args"`
|
||||||
|
Want want `json:"want"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
Timeout string `json:"timeout,omitempty"`
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
Property string `json:"property,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
JSONPath string `json:"jsonpath,omitempty"`
|
||||||
|
Response string `json:"response,omitempty"`
|
||||||
|
StatusCode int `json:"statuscode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type want struct {
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Err string `json:"err,omitempty"`
|
||||||
|
Result string `json:"result,omitempty"`
|
||||||
|
ResultMap map[string]string `json:"resultmap,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var testCases = `
|
||||||
|
case: error url
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id={{ .unclosed.template
|
||||||
|
want:
|
||||||
|
err: failed to parse url
|
||||||
|
---
|
||||||
|
case: error body
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
|
||||||
|
body: Body error {{ .unclosed.template
|
||||||
|
want:
|
||||||
|
err: failed to parse body
|
||||||
|
---
|
||||||
|
case: error connection
|
||||||
|
args:
|
||||||
|
url: 1/api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
|
||||||
|
want:
|
||||||
|
err: failed to call endpoint
|
||||||
|
---
|
||||||
|
case: error no secret err
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id=testkey&version=1
|
||||||
|
key: testkey
|
||||||
|
version: 1
|
||||||
|
statuscode: 404
|
||||||
|
response: not found
|
||||||
|
want:
|
||||||
|
path: /api/getsecret?id=testkey&version=1
|
||||||
|
err: ` + esv1beta1.NoSecretErr.Error() + `
|
||||||
|
---
|
||||||
|
case: error server error
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id=testkey&version=1
|
||||||
|
key: testkey
|
||||||
|
version: 1
|
||||||
|
statuscode: 500
|
||||||
|
response: server error
|
||||||
|
want:
|
||||||
|
path: /api/getsecret?id=testkey&version=1
|
||||||
|
err: endpoint gave error 500
|
||||||
|
---
|
||||||
|
case: error bad json
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id=testkey&version=1
|
||||||
|
key: testkey
|
||||||
|
version: 1
|
||||||
|
jsonpath: $.result.thesecret
|
||||||
|
response: '{"result":{"thesecret":"secret-value"}'
|
||||||
|
want:
|
||||||
|
path: /api/getsecret?id=testkey&version=1
|
||||||
|
err: failed to parse response json
|
||||||
|
---
|
||||||
|
case: error bad jsonpath
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id=testkey&version=1
|
||||||
|
key: testkey
|
||||||
|
version: 1
|
||||||
|
jsonpath: $.result.thesecret
|
||||||
|
response: '{"result":{"nosecret":"secret-value"}}'
|
||||||
|
want:
|
||||||
|
path: /api/getsecret?id=testkey&version=1
|
||||||
|
err: failed to get response path
|
||||||
|
---
|
||||||
|
case: pull data out of map
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id=testkey&version=1
|
||||||
|
key: testkey
|
||||||
|
version: 1
|
||||||
|
jsonpath: $.result.thesecret
|
||||||
|
response: '{"result":{"thesecret":{"one":"secret-value"}}}'
|
||||||
|
want:
|
||||||
|
path: /api/getsecret?id=testkey&version=1
|
||||||
|
err: ''
|
||||||
|
result: '{"one":"secret-value"}'
|
||||||
|
---
|
||||||
|
case: not valid response path
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id=testkey&version=1
|
||||||
|
key: testkey
|
||||||
|
version: 1
|
||||||
|
jsonpath: $.result.unexisting
|
||||||
|
response: '{"result":{"thesecret":{"one":"secret-value"}}}'
|
||||||
|
want:
|
||||||
|
path: /api/getsecret?id=testkey&version=1
|
||||||
|
err: 'failed to get response path'
|
||||||
|
result: ''
|
||||||
|
---
|
||||||
|
case: response path not json
|
||||||
|
args:
|
||||||
|
url: /api/getsecret?id=testkey&version=1
|
||||||
|
key: testkey
|
||||||
|
version: 1
|
||||||
|
jsonpath: $.result.thesecret
|
||||||
|
response: '{"result":{"thesecret":[{"one":"secret-value"}]}}'
|
||||||
|
want:
|
||||||
|
path: /api/getsecret?id=testkey&version=1
|
||||||
|
err: 'failed to get response (wrong type:'
|
||||||
|
result: ''
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestWebhookGetSecret(t *testing.T) {
|
||||||
|
ydec := yaml.NewDecoder(bytes.NewReader([]byte(testCases)))
|
||||||
|
for {
|
||||||
|
var tc testCase
|
||||||
|
if err := ydec.Decode(&tc); err != nil {
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Errorf("testcase decode error %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
runTestCase(tc, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCaseServer(tc testCase, t *testing.T) *httptest.Server {
|
||||||
|
// Start a new server for every test case because the server wants to check the expected api path
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if tc.Want.Path != "" && req.URL.String() != tc.Want.Path {
|
||||||
|
t.Errorf("%s: unexpected api path: %s, expected %s", tc.Case, req.URL.String(), tc.Want.Path)
|
||||||
|
}
|
||||||
|
if tc.Args.StatusCode != 0 {
|
||||||
|
rw.WriteHeader(tc.Args.StatusCode)
|
||||||
|
}
|
||||||
|
rw.Write([]byte(tc.Args.Response))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTimeout(timeout string) (*metav1.Duration, error) {
|
||||||
|
if timeout == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
dur, err := time.ParseDuration(timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &metav1.Duration{Duration: dur}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestCase(tc testCase, t *testing.T) {
|
||||||
|
ts := testCaseServer(tc, t)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
testStore := makeGenerator(ts.URL, tc.Args)
|
||||||
|
jsonRes, err := json.Marshal(testStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
genSpec := &apiextensions.JSON{Raw: jsonRes}
|
||||||
|
timeout, err := parseTimeout(tc.Args.Timeout)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testStore.Spec.Timeout = timeout
|
||||||
|
testProv := &Webhook{}
|
||||||
|
testGenerate(tc, t, testProv, genSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGenerate(tc testCase, t *testing.T, client genv1alpha1.Generator, testStore *apiextensions.JSON) {
|
||||||
|
secretmap, err := client.Generate(context.Background(), testStore, nil, "testnamespace")
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
if (tc.Want.Err == "") != (errStr == "") || !strings.Contains(errStr, tc.Want.Err) {
|
||||||
|
t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
for wantkey, wantval := range tc.Want.ResultMap {
|
||||||
|
gotval, ok := secretmap[wantkey]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s: unexpected response: wanted key '%s' not found", tc.Case, wantkey)
|
||||||
|
} else if string(gotval) != wantval {
|
||||||
|
t.Errorf("%s: unexpected response: key '%s' = '%s' (expected '%s')", tc.Case, wantkey, wantval, gotval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGenerator(url string, args args) *genv1alpha1.Webhook {
|
||||||
|
store := &genv1alpha1.Webhook{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Webhook",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "wehbook-store",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: genv1alpha1.WebhookSpec{
|
||||||
|
URL: url + args.URL,
|
||||||
|
Body: args.Body,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application.json",
|
||||||
|
"X-SecretKey": "{{ .remoteRef.key }}",
|
||||||
|
},
|
||||||
|
Result: genv1alpha1.WebhookResult{
|
||||||
|
JSONPath: args.JSONPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
|
@ -15,17 +15,10 @@ limitations under the License.
|
||||||
package webhook
|
package webhook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
tpl "text/template"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/PaesslerAG/jsonpath"
|
"github.com/PaesslerAG/jsonpath"
|
||||||
|
@ -34,12 +27,8 @@ import (
|
||||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||||
|
|
||||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
"github.com/external-secrets/external-secrets/pkg/common/webhook"
|
||||||
"github.com/external-secrets/external-secrets/pkg/constants"
|
|
||||||
"github.com/external-secrets/external-secrets/pkg/metrics"
|
|
||||||
"github.com/external-secrets/external-secrets/pkg/template/v2"
|
|
||||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||||
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://github.com/external-secrets/external-secrets/issues/644
|
// https://github.com/external-secrets/external-secrets/issues/644
|
||||||
|
@ -50,11 +39,9 @@ var _ esv1beta1.Provider = &Provider{}
|
||||||
type Provider struct{}
|
type Provider struct{}
|
||||||
|
|
||||||
type WebHook struct {
|
type WebHook struct {
|
||||||
kube client.Client
|
wh webhook.Webhook
|
||||||
store esv1beta1.GenericStore
|
store esv1beta1.GenericStore
|
||||||
namespace string
|
|
||||||
storeKind string
|
storeKind string
|
||||||
http *http.Client
|
|
||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,19 +57,25 @@ func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) NewClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
|
func (p *Provider) NewClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
|
||||||
|
wh := webhook.Webhook{
|
||||||
|
Kube: kube,
|
||||||
|
Namespace: namespace,
|
||||||
|
}
|
||||||
whClient := &WebHook{
|
whClient := &WebHook{
|
||||||
kube: kube,
|
|
||||||
store: store,
|
store: store,
|
||||||
namespace: namespace,
|
wh: wh,
|
||||||
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
|
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
|
||||||
}
|
}
|
||||||
|
if whClient.storeKind == esv1beta1.ClusterSecretStoreKind {
|
||||||
|
whClient.wh.ClusterScoped = true
|
||||||
|
}
|
||||||
provider, err := getProvider(store)
|
provider, err := getProvider(store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
whClient.url = provider.URL
|
whClient.url = provider.URL
|
||||||
|
|
||||||
whClient.http, err = whClient.getHTTPClient(provider)
|
whClient.wh.HTTP, err = whClient.wh.GetHTTPClient(provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -93,30 +86,18 @@ func (p *Provider) ValidateStore(_ esv1beta1.GenericStore) (admission.Warnings,
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProvider(store esv1beta1.GenericStore) (*esv1beta1.WebhookProvider, error) {
|
func getProvider(store esv1beta1.GenericStore) (*webhook.Spec, error) {
|
||||||
spc := store.GetSpec()
|
spc := store.GetSpec()
|
||||||
if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {
|
if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {
|
||||||
return nil, fmt.Errorf("missing store provider webhook")
|
return nil, fmt.Errorf("missing store provider webhook")
|
||||||
}
|
}
|
||||||
return spc.Provider.Webhook, nil
|
out := webhook.Spec{}
|
||||||
}
|
d, err := json.Marshal(spc.Provider.Webhook)
|
||||||
|
if err != nil {
|
||||||
func (w *WebHook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelector) (*corev1.Secret, error) {
|
return nil, err
|
||||||
ke := client.ObjectKey{
|
|
||||||
Name: ref.Name,
|
|
||||||
Namespace: w.namespace,
|
|
||||||
}
|
}
|
||||||
if w.storeKind == esv1beta1.ClusterSecretStoreKind {
|
err = json.Unmarshal(d, &out)
|
||||||
if ref.Namespace == nil {
|
return &out, err
|
||||||
return nil, fmt.Errorf("no namespace on ClusterSecretStore webhook secret %s", ref.Name)
|
|
||||||
}
|
|
||||||
ke.Namespace = *ref.Namespace
|
|
||||||
}
|
|
||||||
secret := &corev1.Secret{}
|
|
||||||
if err := w.kube.Get(ctx, ke, secret); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
|
|
||||||
}
|
|
||||||
return secret, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebHook) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
|
func (w *WebHook) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
|
||||||
|
@ -139,16 +120,16 @@ func (w *WebHook) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDat
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get store: %w", err)
|
return nil, fmt.Errorf("failed to get store: %w", err)
|
||||||
}
|
}
|
||||||
result, err := w.getWebhookData(ctx, provider, ref)
|
result, err := w.wh.GetWebhookData(ctx, provider, &ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Only parse as json if we have a jsonpath set
|
// Only parse as json if we have a jsonpath set
|
||||||
data, err := w.getTemplateData(ctx, ref, provider.Secrets)
|
data, err := w.wh.GetTemplateData(ctx, &ref, provider.Secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resultJSONPath, err := executeTemplateString(provider.Result.JSONPath, data)
|
resultJSONPath, err := webhook.ExecuteTemplateString(provider.Result.JSONPath, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -207,229 +188,7 @@ func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecret
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get store: %w", err)
|
return nil, fmt.Errorf("failed to get store: %w", err)
|
||||||
}
|
}
|
||||||
result, err := w.getWebhookData(ctx, provider, ref)
|
return w.wh.GetSecretMap(ctx, provider, &ref)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We always want json here, so just parse it out
|
|
||||||
jsondata := interface{}(nil)
|
|
||||||
if err := json.Unmarshal(result, &jsondata); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse response json: %w", err)
|
|
||||||
}
|
|
||||||
// Get subdata via jsonpath, if given
|
|
||||||
if provider.Result.JSONPath != "" {
|
|
||||||
jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the value is a string, try to parse it as json
|
|
||||||
jsonstring, ok := jsondata.(string)
|
|
||||||
if ok {
|
|
||||||
// This could also happen if the response was a single json-encoded string
|
|
||||||
// but that is an extremely unlikely scenario
|
|
||||||
if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Use the data as a key-value map
|
|
||||||
jsonvalue, ok := jsondata.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change the map of generic objects to a map of byte arrays
|
|
||||||
values := make(map[string][]byte)
|
|
||||||
for rKey, rValue := range jsonvalue {
|
|
||||||
jVal, ok := rValue.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to get response (wrong type in key '%s': %T)", rKey, rValue)
|
|
||||||
}
|
|
||||||
values[rKey] = []byte(jVal)
|
|
||||||
}
|
|
||||||
return values, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebHook) getTemplateData(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef, secrets []esv1beta1.WebhookSecret) (map[string]map[string]string, error) {
|
|
||||||
data := map[string]map[string]string{
|
|
||||||
"remoteRef": {
|
|
||||||
"key": url.QueryEscape(ref.Key),
|
|
||||||
"version": url.QueryEscape(ref.Version),
|
|
||||||
"property": url.QueryEscape(ref.Property),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, secref := range secrets {
|
|
||||||
if _, ok := data[secref.Name]; !ok {
|
|
||||||
data[secref.Name] = make(map[string]string)
|
|
||||||
}
|
|
||||||
secret, err := w.getStoreSecret(ctx, secref.SecretRef)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for sKey, sVal := range secret.Data {
|
|
||||||
data[secref.Name][sKey] = string(sVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebHook) getWebhookData(ctx context.Context, provider *esv1beta1.WebhookProvider, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
|
||||||
if w.http == nil {
|
|
||||||
return nil, fmt.Errorf("http client not initialized")
|
|
||||||
}
|
|
||||||
data, err := w.getTemplateData(ctx, ref, provider.Secrets)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
method := provider.Method
|
|
||||||
if method == "" {
|
|
||||||
method = http.MethodGet
|
|
||||||
}
|
|
||||||
url, err := executeTemplateString(provider.URL, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse url: %w", err)
|
|
||||||
}
|
|
||||||
body, err := executeTemplate(provider.Body, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, url, &body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
for hKey, hValueTpl := range provider.Headers {
|
|
||||||
hValue, err := executeTemplateString(hValueTpl, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
|
|
||||||
}
|
|
||||||
req.Header.Add(hKey, hValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := w.http.Do(req)
|
|
||||||
metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to call endpoint: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode == 404 {
|
|
||||||
return nil, esv1beta1.NoSecretError{}
|
|
||||||
}
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
|
|
||||||
}
|
|
||||||
return io.ReadAll(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebHook) getHTTPClient(provider *esv1beta1.WebhookProvider) (*http.Client, error) {
|
|
||||||
client := &http.Client{}
|
|
||||||
if provider.Timeout != nil {
|
|
||||||
client.Timeout = provider.Timeout.Duration
|
|
||||||
}
|
|
||||||
if len(provider.CABundle) == 0 && provider.CAProvider == nil {
|
|
||||||
// No need to process ca stuff if it is not there
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
caCertPool, err := w.getCACertPool(provider)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf := &tls.Config{
|
|
||||||
RootCAs: caCertPool,
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
}
|
|
||||||
client.Transport = &http.Transport{TLSClientConfig: tlsConf}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebHook) getCACertPool(provider *esv1beta1.WebhookProvider) (*x509.CertPool, error) {
|
|
||||||
caCertPool := x509.NewCertPool()
|
|
||||||
if len(provider.CABundle) > 0 {
|
|
||||||
ok := caCertPool.AppendCertsFromPEM(provider.CABundle)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to append cabundle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.CAProvider != nil && w.storeKind == esv1beta1.ClusterSecretStoreKind && provider.CAProvider.Namespace == nil {
|
|
||||||
return nil, fmt.Errorf("missing namespace on CAProvider secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.CAProvider != nil {
|
|
||||||
var cert []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch provider.CAProvider.Type {
|
|
||||||
case esv1beta1.WebhookCAProviderTypeSecret:
|
|
||||||
cert, err = w.getCertFromSecret(provider)
|
|
||||||
case esv1beta1.WebhookCAProviderTypeConfigMap:
|
|
||||||
cert, err = w.getCertFromConfigMap(provider)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknown caprovider type: %s", provider.CAProvider.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ok := caCertPool.AppendCertsFromPEM(cert)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to append cabundle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return caCertPool, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebHook) getCertFromSecret(provider *esv1beta1.WebhookProvider) ([]byte, error) {
|
|
||||||
secretRef := esmeta.SecretKeySelector{
|
|
||||||
Name: provider.CAProvider.Name,
|
|
||||||
Namespace: &w.namespace,
|
|
||||||
Key: provider.CAProvider.Key,
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.CAProvider.Namespace != nil {
|
|
||||||
secretRef.Namespace = provider.CAProvider.Namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
cert, err := resolvers.SecretKeyRef(
|
|
||||||
ctx,
|
|
||||||
w.kube,
|
|
||||||
w.storeKind,
|
|
||||||
w.namespace,
|
|
||||||
&secretRef,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(cert), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WebHook) getCertFromConfigMap(provider *esv1beta1.WebhookProvider) ([]byte, error) {
|
|
||||||
objKey := client.ObjectKey{
|
|
||||||
Name: provider.CAProvider.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.CAProvider.Namespace != nil {
|
|
||||||
objKey.Namespace = *provider.CAProvider.Namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
configMapRef := &corev1.ConfigMap{}
|
|
||||||
ctx := context.Background()
|
|
||||||
err := w.kube.Get(ctx, objKey, configMapRef)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get caprovider secret %s: %w", objKey.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok := configMapRef.Data[provider.CAProvider.Key]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to get caprovider configmap %s -> %s", objKey.Name, provider.CAProvider.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(val), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebHook) Close(_ context.Context) error {
|
func (w *WebHook) Close(_ context.Context) error {
|
||||||
|
@ -445,26 +204,3 @@ func (w *WebHook) Validate() (esv1beta1.ValidationResult, error) {
|
||||||
}
|
}
|
||||||
return esv1beta1.ValidationResultReady, nil
|
return esv1beta1.ValidationResultReady, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
|
|
||||||
result, err := executeTemplate(tmpl, data)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return result.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
|
|
||||||
var result bytes.Buffer
|
|
||||||
if tmpl == "" {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
if err := urlt.Execute(&result, data); err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue