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:
commit
44d4cf061b
12 changed files with 1297 additions and 2 deletions
|
@ -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 {
|
||||
|
|
101
apis/externalsecrets/v1alpha1/secretstore_webhook_types.go
Normal file
101
apis/externalsecrets/v1alpha1/secretstore_webhook_types.go
Normal 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"`
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -927,6 +927,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
|
||||
|
|
|
@ -927,6 +927,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
|
||||
|
|
118
docs/provider-webhook.md
Normal file
118
docs/provider-webhook.md
Normal 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
13
go.mod
|
@ -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
15
go.sum
|
@ -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=
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
401
pkg/provider/webhook/webhook.go
Normal file
401
pkg/provider/webhook/webhook.go
Normal 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
|
||||
}
|
340
pkg/provider/webhook/webhook_test.go
Normal file
340
pkg/provider/webhook/webhook_test.go
Normal 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
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue