1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

Added generic webhook provider

This provider allows a secretstore with a generic url (templated)
which will be called with a defined method, headers (templated)
and optional body (also templated)
The response can be parsed out with a jsonPath expression
This commit is contained in:
Willem Monsuwe 2021-12-29 10:53:29 +01:00
parent 3b9bbfd1f6
commit d04508e974
11 changed files with 1133 additions and 2 deletions

View file

@ -77,6 +77,10 @@ type SecretStoreProvider struct {
// Alibaba configures this store to sync secrets using Alibaba Cloud provider
// +optional
Alibaba *AlibabaProvider `json:"alibaba,omitempty"`
// Webhook configures this store to sync secrets using a generic templated webhook
// +optional
Webhook *WebhookProvider `json:"webhook,omitempty"`
}
type SecretStoreRetrySettings struct {

View file

@ -0,0 +1,101 @@
/*
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"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)
// AkeylessProvider Configures an store to sync secrets using Akeyless KV.
type WebhookProvider 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 esmeta.SecretKeySelector `json:"secretRef"`
}

View file

@ -934,6 +934,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(AlibabaProvider)
(*in).DeepCopyInto(*out)
}
if in.Webhook != nil {
in, out := &in.Webhook, &out.Webhook
*out = new(WebhookProvider)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@ -1275,6 +1280,102 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
return out
}
// 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 *WebhookProvider) DeepCopyInto(out *WebhookProvider) {
*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(v1.Duration)
**out = **in
}
out.Result = in.Result
if in.Secrets != nil {
in, out := &in.Secrets, &out.Secrets
*out = make([]WebhookSecret, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
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 WebhookProvider.
func (in *WebhookProvider) DeepCopy() *WebhookProvider {
if in == nil {
return nil
}
out := new(WebhookProvider)
in.DeepCopyInto(out)
return out
}
// 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
in.SecretRef.DeepCopyInto(&out.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 *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
*out = *in

View file

@ -914,6 +914,106 @@ spec:
- path
- server
type: object
webhook:
description: Webhook configures this store to sync secrets using
a generic templated webhook
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 of the entry in the Secret
resource's `data` field to be used. Some instances
of this field may be defaulted, in others it may
be required.
type: string
name:
description: The name of the Secret resource being
referred to.
type: string
namespace:
description: Namespace of the resource being referred
to. Ignored if referent is not cluster-scoped.
cluster-scoped defaults to the namespace of the
referent.
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
yandexlockbox:
description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider

View file

@ -914,6 +914,106 @@ spec:
- path
- server
type: object
webhook:
description: Webhook configures this store to sync secrets using
a generic templated webhook
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 of the entry in the Secret
resource's `data` field to be used. Some instances
of this field may be defaulted, in others it may
be required.
type: string
name:
description: The name of the Secret resource being
referred to.
type: string
namespace:
description: Namespace of the resource being referred
to. Ignored if referent is not cluster-scoped.
cluster-scoped defaults to the namespace of the
referent.
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
yandexlockbox:
description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider

13
go.mod
View file

@ -4,6 +4,7 @@ go 1.17
replace (
github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1 => ./apis/externalsecrets/v1alpha1
github.com/external-secrets/external-secrets/e2e/framework/log => ./e2e/framework/log
github.com/external-secrets/external-secrets/pkg/provider/gitlab => ./pkg/provider/gitlab
google.golang.org/grpc => google.golang.org/grpc v1.27.0
k8s.io/api => k8s.io/api v0.21.2
@ -38,6 +39,10 @@ require (
github.com/Azure/go-autorest/autorest/azure/auth v0.5.7
github.com/IBM/go-sdk-core/v5 v5.5.0
github.com/IBM/secrets-manager-go-sdk v1.0.23
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/PaesslerAG/jsonpath v0.1.1
github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2
github.com/akeylesslabs/akeyless-go/v2 v2.5.11
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192
@ -49,6 +54,8 @@ require (
github.com/google/uuid v1.2.0
github.com/googleapis/gax-go v1.0.3
github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4
github.com/huandu/xstrings v1.3.2 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/lestrrat-go/jwx v1.2.1
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.16.0
@ -67,6 +74,7 @@ require (
google.golang.org/api v0.45.0
google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3
google.golang.org/grpc v1.43.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919
k8s.io/api v0.21.3
k8s.io/apimachinery v0.21.3
@ -88,6 +96,7 @@ require (
github.com/Azure/go-autorest/logger v0.2.0 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/PaesslerAG/gval v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/aws/aws-sdk-go-v2 v0.23.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@ -133,7 +142,6 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
@ -143,8 +151,10 @@ require (
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
@ -184,7 +194,6 @@ require (
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
honnef.co/go/tools v0.1.4 // indirect
k8s.io/apiextensions-apiserver v0.21.2 // indirect
k8s.io/component-base v0.21.2 // indirect

15
go.sum
View file

@ -72,9 +72,20 @@ github.com/IBM/go-sdk-core/v5 v5.5.0 h1:etP4m0kzMCxjZRI4Bu6cRTfK9YDvY3xFuagXugkC
github.com/IBM/go-sdk-core/v5 v5.5.0/go.mod h1:Sn+z+qTDREQvCr+UFa22TqqfXNxx3o723y8GsfLV8e0=
github.com/IBM/secrets-manager-go-sdk v1.0.23 h1:YvRB2jmCfXVwTiTozCNVIRfl6q9Qcl2JiL4x6chOSI4=
github.com/IBM/secrets-manager-go-sdk v1.0.23/go.mod h1:ruP6eQ0/J/zHBbnMfUyWeMsTe9vgnGL4rDeLiSKhZhU=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2 h1:1h4udX3Y5KgSG0m4Th2bHfaYxZB9fbngiij9PrKEp6c=
@ -414,6 +425,8 @@ github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 h1:e1ok06zG
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
@ -507,6 +520,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@ -522,6 +536,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=

View file

@ -26,5 +26,6 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
)

View file

@ -0,0 +1,382 @@
/*
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"
"fmt"
"io"
"net/http"
"net/url"
"strings"
tpl "text/template"
"github.com/Masterminds/sprig"
"github.com/PaesslerAG/jsonpath"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/template"
)
// Provider satisfies the provider interface.
type Provider struct{}
type WebHook struct {
kube client.Client
store esv1alpha1.GenericStore
namespace string
storeKind string
}
func init() {
schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
Webhook: &esv1alpha1.WebhookProvider{},
})
}
func (p *Provider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
whClient := &WebHook{
kube: kube,
store: store,
namespace: namespace,
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
}
return whClient, nil
}
func getProvider(store esv1alpha1.GenericStore) (*esv1alpha1.WebhookProvider, error) {
spc := store.GetSpec()
if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {
return nil, fmt.Errorf("missing store provider webhook")
}
return spc.Provider.Webhook, nil
}
func (w *WebHook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelector) (*corev1.Secret, error) {
ke := client.ObjectKey{
Name: ref.Name,
Namespace: w.namespace,
}
if w.storeKind == esv1alpha1.ClusterSecretStoreKind {
if ref.Namespace == nil {
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) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
provider, err := getProvider(w.store)
if err != nil {
return nil, fmt.Errorf("failed to get store: %w", err)
}
result, err := w.getWebhookData(ctx, provider, ref)
if err != nil {
return nil, err
}
if provider.Result.JSONPath != "" {
jsondata := interface{}(nil)
if err := yaml.Unmarshal(result, &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse response json: %w", err)
}
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)
}
jsonvalue, ok := jsondata.(string)
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
}
return []byte(jsonvalue), nil
}
return result, nil
}
func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
provider, err := getProvider(w.store)
if err != nil {
return nil, fmt.Errorf("failed to get store: %w", err)
}
result, err := w.getWebhookData(ctx, provider, ref)
if err != nil {
return nil, err
}
jsondata := interface{}(nil)
var jsonvalue map[string]interface{}
var ok bool
if provider.Result.JSONPath != "" {
if err := yaml.Unmarshal(result, &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse response json: %w", err)
}
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)
}
jsonvalue, ok = jsondata.(map[string]interface{})
if !ok {
jsonstring, ok := jsondata.(string)
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
}
if err := yaml.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse data json: %w", err)
}
jsonvalue, ok = jsondata.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type in data: %T)", jsondata)
}
}
} else {
if err := yaml.Unmarshal(result, &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse data json: %w", err)
}
jsonvalue, ok = jsondata.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type in body: %T)", jsondata)
}
}
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: %T)", rValue)
}
values[rKey] = []byte(jVal)
}
return values, nil
}
func (w *WebHook) getWebhookData(ctx context.Context, provider *esv1alpha1.WebhookProvider, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
data := map[string]map[string]string{
"remoteRef": {
"key": url.QueryEscape(ref.Key),
"version": url.QueryEscape(ref.Version),
},
}
if provider.Secrets != nil {
for _, secref := range provider.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)
}
}
}
method := provider.Method
if method == "" {
method = "GET"
}
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)
}
if provider.Headers != nil {
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)
}
}
client, err := w.getHTTPClient(ctx, provider)
if err != nil {
return nil, fmt.Errorf("failed to call endpoint: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to call endpoint: %w", err)
}
defer resp.Body.Close()
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(_ context.Context, provider *esv1alpha1.WebhookProvider) (*http.Client, error) {
client := &http.Client{}
if provider.Timeout != nil {
client.Timeout = provider.Timeout.Duration
}
if len(provider.CABundle) != 0 || provider.CAProvider != nil {
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 == esv1alpha1.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 esv1alpha1.WebhookCAProviderTypeSecret:
cert, err = w.getCertFromSecret(provider)
case esv1alpha1.WebhookCAProviderTypeConfigMap:
cert, err = w.getCertFromConfigMap(provider)
default:
return nil, 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")
}
}
tlsConf := &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
client.Transport = &http.Transport{TLSClientConfig: tlsConf}
}
return client, nil
}
func (w *WebHook) getCertFromSecret(provider *esv1alpha1.WebhookProvider) ([]byte, error) {
secretRef := esmeta.SecretKeySelector{
Name: provider.CAProvider.Name,
Key: provider.CAProvider.Key,
}
if provider.CAProvider.Namespace != nil {
secretRef.Namespace = provider.CAProvider.Namespace
}
ctx := context.Background()
res, err := w.secretKeyRef(ctx, &secretRef)
if err != nil {
return nil, err
}
return []byte(res), nil
}
func (w *WebHook) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
secret := &corev1.Secret{}
ref := client.ObjectKey{
Namespace: w.namespace,
Name: secretRef.Name,
}
if (w.storeKind == esv1alpha1.ClusterSecretStoreKind) &&
(secretRef.Namespace != nil) {
ref.Namespace = *secretRef.Namespace
}
err := w.kube.Get(ctx, ref, secret)
if err != nil {
return "", err
}
keyBytes, ok := secret.Data[secretRef.Key]
if !ok {
return "", err
}
value := string(keyBytes)
valueStr := strings.TrimSpace(value)
return valueStr, nil
}
func (w *WebHook) getCertFromConfigMap(provider *esv1alpha1.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(ctx context.Context) error {
return 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(sprig.TxtFuncMap()).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
}

View file

@ -0,0 +1,313 @@
/*
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"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"gopkg.in/yaml.v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/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"`
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 not found
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
statuscode: 404
response: not found
want:
path: /api/getsecret?id=testkey&version=1
err: endpoint gave error 404
---
case: error bad json
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
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={{ .remoteRef.key }}&version={{ .remoteRef.version }}
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: error bad json data
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
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
---
case: error timeout
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
response: secret-value
timeout: 0.01ms
want:
path: /api/getsecret?id=testkey&version=1
err: context deadline exceeded
---
case: good plaintext
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
response: secret-value
want:
path: /api/getsecret?id=testkey&version=1
result: secret-value
---
case: good json
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
jsonpath: $.result.thesecret
response: '{"result":{"thesecret":"secret-value"}}'
want:
path: /api/getsecret?id=testkey&version=1
result: secret-value
---
case: good json map
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
jsonpath: $.result
response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
want:
path: /api/getsecret?id=testkey&version=1
resultmap:
thesecret: secret-value
alsosecret: another-value
---
case: good json map string
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
response: '{"thesecret":"secret-value","alsosecret":"another-value"}'
want:
path: /api/getsecret?id=testkey&version=1
resultmap:
thesecret: secret-value
alsosecret: another-value
---
case: error json map string
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
response: 'some simple string'
want:
path: /api/getsecret?id=testkey&version=1
err: failed to get response (wrong type in body
resultmap:
thesecret: secret-value
alsosecret: another-value
---
case: error json map
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
key: testkey
version: 1
jsonpath: $.result.thesecret
response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
want:
path: /api/getsecret?id=testkey&version=1
err: failed to get response (wrong type in data
resultmap:
thesecret: secret-value
alsosecret: another-value
`
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 %w", err)
}
break
}
runTestCase(tc, t)
}
}
func runTestCase(tc testCase, t *testing.T) {
// Start a new server for every test case because the server wants to check the expected api path
ts := 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))
}))
defer ts.Close()
testStore := makeClusterSecretStore(ts.URL, tc.Args)
if tc.Args.Timeout != "" {
dur, err := time.ParseDuration(tc.Args.Timeout)
if err != nil {
t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
return
}
testStore.Spec.Provider.Webhook.Timeout = &metav1.Duration{Duration: dur}
}
testProv := &Provider{}
client, err := testProv.NewClient(context.Background(), testStore, nil, "testnamespace")
if err != nil {
t.Errorf("%s: error creating client: %s", tc.Case, err.Error())
return
}
testRef := esv1alpha1.ExternalSecretDataRemoteRef{
Key: tc.Args.Key,
Version: tc.Args.Version,
}
if tc.Want.ResultMap != nil {
secretmap, err := client.GetSecretMap(context.Background(), testRef)
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)
}
}
}
} else {
secret, err := client.GetSecret(context.Background(), testRef)
errStr := ""
if err != nil {
errStr = err.Error()
}
if !strings.Contains(errStr, tc.Want.Err) {
t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
}
if err == nil && string(secret) != tc.Want.Result {
t.Errorf("%s: unexpected response: '%s' (expected '%s')", tc.Case, secret, tc.Want.Result)
}
}
}
func makeClusterSecretStore(url string, args args) *esv1alpha1.ClusterSecretStore {
store := &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
Kind: "ClusterSecretStore",
},
ObjectMeta: metav1.ObjectMeta{
Name: "wehbook-store",
Namespace: "default",
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
Webhook: &esv1alpha1.WebhookProvider{
URL: url + args.URL,
Body: args.Body,
Headers: map[string]string{
"Content-Type": "application.json",
"X-SecretKey": "{{ .remoteRef.key }}",
},
Result: esv1alpha1.WebhookResult{
JSONPath: args.JSONPath,
},
},
},
},
}
return store
}

View file

@ -51,6 +51,11 @@ var tplFuncs = tpl.FuncMap{
"lower": strings.ToLower,
}
// So other templating calls can use the same extra functions.
func FuncMap() tpl.FuncMap {
return tplFuncs
}
const (
errParse = "unable to parse template at key %s: %s"
errExecute = "unable to execute template at key %s: %s"