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

Merge pull request #559 from willemm/feat/generic_webhook

Add generic webhook provider
This commit is contained in:
paul-the-alien[bot] 2022-01-11 15:50:05 +00:00 committed by GitHub
commit 44d4cf061b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1297 additions and 2 deletions

View file

@ -77,6 +77,10 @@ type SecretStoreProvider struct {
// Alibaba configures this store to sync secrets using Alibaba Cloud provider // Alibaba configures this store to sync secrets using Alibaba Cloud provider
// +optional // +optional
Alibaba *AlibabaProvider `json:"alibaba,omitempty"` 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 { 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) *out = new(AlibabaProvider)
(*in).DeepCopyInto(*out) (*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. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@ -1275,6 +1280,102 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) { func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
*out = *in *out = *in

View file

@ -927,6 +927,106 @@ spec:
- path - path
- server - server
type: object 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: yandexlockbox:
description: YandexLockbox configures this store to sync secrets description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider using Yandex Lockbox provider

View file

@ -927,6 +927,106 @@ spec:
- path - path
- server - server
type: object 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: yandexlockbox:
description: YandexLockbox configures this store to sync secrets description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider using Yandex Lockbox provider

118
docs/provider-webhook.md Normal file
View file

@ -0,0 +1,118 @@
## Generic Webhook
External Secrets Operator can integrate with simple web apis by specifying the endpoint
### Example
First, create a SecretStore with a webhook backend. We'll use a static user/password `root`:
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: webhook-backend
spec:
provider:
webhook:
url: "http://httpbin.org/get?parameter={{ .remoteRef.key }}"
result:
jsonPath: "$.args.parameter"
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
data:
username: dGVzdA== # "test"
password: dGVzdA== # "test"
```
NB: This is obviously not practical because it just returns the key as the result, but it shows how it works
Now create an ExternalSecret that uses the above SecretStore:
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: webhook-example
spec:
refreshInterval: "15s"
secretStoreRef:
name: webhook-backend
kind: SecretStore
target:
name: example-sync
data:
- secretKey: foobar
remoteRef:
key: secret
---
# will create a secret with:
kind: Secret
metadata:
name: example-sync
data:
foobar: c2VjcmV0
```
#### Limitations
Webhook does not support authorization, other than what can be sent by generating http headers
### Templating
Generic WebHook provider uses the templating engine to generate the API call. It can be used in the url, headers, body and result.jsonPath fields.
The provider inserts the secret to be retrieved in the object named `remoteRef`.
In addition, secrets can be added as named objects, for example to use in authorization headers.
Each secret has a `name` property which determines the name of the object in the templating engine.
### All Parameters
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: ClusterSecretStore
metadata:
name: statervault
spec:
provider:
webhook:
# Url to call. Use templating engine to fill in the request parameters
url: <url>
# http method, defaults to GET
method: <method>
# Timeout in duration (1s, 1m, etc)
timeout: 1s
result:
# [jsonPath](https://jsonpath.com) syntax, which also can be templated
jsonPath: <jsonPath>
# Map of headers, can be templated
headers:
<Header-Name>: <header contents>
# Body to sent as request, can be templated (optional)
body: <body>
# List of secrets to expose to the templating engine
secrets:
# Use this name to refer to this secret in templating, above
- name: <name>
secretRef:
namespace: <namespace>
name: <name>
# Add CAs here for the TLS handshake
caBundle: <base64 encoded cabundle>
caProvider:
type: Secret or COnfigMap
name: <name of secret or configmap>
namespace: <namespace>
key: <key inside secret>
```

13
go.mod
View file

@ -4,6 +4,7 @@ go 1.17
replace ( replace (
github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1 => ./apis/externalsecrets/v1alpha1 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 github.com/external-secrets/external-secrets/pkg/provider/gitlab => ./pkg/provider/gitlab
google.golang.org/grpc => google.golang.org/grpc v1.27.0 google.golang.org/grpc => google.golang.org/grpc v1.27.0
k8s.io/api => k8s.io/api v0.21.2 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/Azure/go-autorest/autorest/azure/auth v0.5.7
github.com/IBM/go-sdk-core/v5 v5.5.0 github.com/IBM/go-sdk-core/v5 v5.5.0
github.com/IBM/secrets-manager-go-sdk v1.0.23 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-cloud-id v0.3.2
github.com/akeylesslabs/akeyless-go/v2 v2.5.11 github.com/akeylesslabs/akeyless-go/v2 v2.5.11
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192 github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192
@ -49,6 +54,8 @@ require (
github.com/google/uuid v1.2.0 github.com/google/uuid v1.2.0
github.com/googleapis/gax-go v1.0.3 github.com/googleapis/gax-go v1.0.3
github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4 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/lestrrat-go/jwx v1.2.1
github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.16.0 github.com/onsi/gomega v1.16.0
@ -67,6 +74,7 @@ require (
google.golang.org/api v0.45.0 google.golang.org/api v0.45.0
google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3 google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3
google.golang.org/grpc v1.43.0 google.golang.org/grpc v1.43.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919
k8s.io/api v0.21.3 k8s.io/api v0.21.3
k8s.io/apimachinery 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/logger v0.2.0 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/BurntSushi/toml v0.3.1 // 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/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/aws/aws-sdk-go-v2 v0.23.0 // indirect github.com/aws/aws-sdk-go-v2 v0.23.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
@ -133,7 +142,6 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.11 // 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/leodido/go-urn v1.2.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect
github.com/lestrrat-go/blackmagic v1.0.0 // 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-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // 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/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/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.3.3 // 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/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // 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/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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 honnef.co/go/tools v0.1.4 // indirect
k8s.io/apiextensions-apiserver v0.21.2 // indirect k8s.io/apiextensions-apiserver v0.21.2 // indirect
k8s.io/component-base 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/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 h1:YvRB2jmCfXVwTiTozCNVIRfl6q9Qcl2JiL4x6chOSI4=
github.com/IBM/secrets-manager-go-sdk v1.0.23/go.mod h1:ruP6eQ0/J/zHBbnMfUyWeMsTe9vgnGL4rDeLiSKhZhU= 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 v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 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/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/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/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= 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/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/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/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-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/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= 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/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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 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/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.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 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.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 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/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 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 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/ibm"
_ "github.com/external-secrets/external-secrets/pkg/provider/oracle" _ "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/vault"
_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox" _ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
) )

View file

@ -0,0 +1,401 @@
/*
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
http *http.Client
}
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,
}
provider, err := getProvider(store)
if err != nil {
return nil, err
}
whClient.http, err = whClient.getHTTPClient(provider)
if err != nil {
return nil, err
}
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
}
// Only parse as json if we have a jsonpath set
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
}
// We always want json here, so just parse it out
jsondata := interface{}(nil)
if err := yaml.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 := yaml.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 esv1alpha1.ExternalSecretDataRemoteRef, secrets []esv1alpha1.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 *esv1alpha1.WebhookProvider, ref esv1alpha1.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)
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(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 {
// 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 *esv1alpha1.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 == 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:
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 *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,340 @@
/*
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"
"github.com/external-secrets/external-secrets/pkg/provider"
)
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
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
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 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 := makeClusterSecretStore(ts.URL, tc.Args)
var err error
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.Provider.Webhook.Timeout = timeout
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
}
if tc.Want.ResultMap != nil {
testGetSecretMap(tc, t, client)
} else {
testGetSecret(tc, t, client)
}
}
func testGetSecretMap(tc testCase, t *testing.T, client provider.SecretsClient) {
testRef := esv1alpha1.ExternalSecretDataRemoteRef{
Key: tc.Args.Key,
Version: tc.Args.Version,
}
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)
}
}
}
}
func testGetSecret(tc testCase, t *testing.T, client provider.SecretsClient) {
testRef := esv1alpha1.ExternalSecretDataRemoteRef{
Key: tc.Args.Key,
Version: tc.Args.Version,
}
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, "lower": strings.ToLower,
} }
// So other templating calls can use the same extra functions.
func FuncMap() tpl.FuncMap {
return tplFuncs
}
const ( const (
errParse = "unable to parse template at key %s: %s" errParse = "unable to parse template at key %s: %s"
errExecute = "unable to execute template at key %s: %s" errExecute = "unable to execute template at key %s: %s"