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

Implements Webhook Generator (#3121)

* adding webhook generators

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* bumping bundle

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* linting

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* fixing copy-paste error

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* common webhook functions

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* removing duplicates. Adding tests for generator

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* docs

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

---------

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
This commit is contained in:
Gustavo Fernandes de Carvalho 2024-02-17 06:49:31 -03:00 committed by GitHub
parent 2ca08fbfb6
commit 1cf8f68276
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1445 additions and 286 deletions

View file

@ -0,0 +1,130 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// WebhookSpec controls the behavior of the external generator. Any body parameters should be passed to the server through the parameters field.
type WebhookSpec struct {
// Webhook Method
// +optional, default GET
Method string `json:"method,omitempty"`
// Webhook url to call
URL string `json:"url"`
// Headers
// +optional
Headers map[string]string `json:"headers,omitempty"`
// Body
// +optional
Body string `json:"body,omitempty"`
// Timeout
// +optional
Timeout *metav1.Duration `json:"timeout,omitempty"`
// Result formatting
Result WebhookResult `json:"result"`
// Secrets to fill in templates
// These secrets will be passed to the templating function as key value pairs under the given name
// +optional
Secrets []WebhookSecret `json:"secrets,omitempty"`
// PEM encoded CA bundle used to validate webhook server certificate. Only used
// if the Server URL is using HTTPS protocol. This parameter is ignored for
// plain HTTP protocol connection. If not set the system root certificates
// are used to validate the TLS connection.
// +optional
CABundle []byte `json:"caBundle,omitempty"`
// The provider for the CA bundle to use to validate webhook server certificate.
// +optional
CAProvider *WebhookCAProvider `json:"caProvider,omitempty"`
}
type WebhookCAProviderType string
const (
WebhookCAProviderTypeSecret WebhookCAProviderType = "Secret"
WebhookCAProviderTypeConfigMap WebhookCAProviderType = "ConfigMap"
)
// Defines a location to fetch the cert for the webhook provider from.
type WebhookCAProvider struct {
// The type of provider to use such as "Secret", or "ConfigMap".
// +kubebuilder:validation:Enum="Secret";"ConfigMap"
Type WebhookCAProviderType `json:"type"`
// The name of the object located at the provider type.
Name string `json:"name"`
// The key the value inside of the provider type to use, only used with "Secret" type
// +kubebuilder:validation:Optional
Key string `json:"key,omitempty"`
// The namespace the Provider type is in.
// +optional
Namespace *string `json:"namespace,omitempty"`
}
type WebhookResult struct {
// Json path of return value
// +optional
JSONPath string `json:"jsonPath,omitempty"`
}
type WebhookSecret struct {
// Name of this secret in templates
Name string `json:"name"`
// Secret ref to fill in credentials
SecretRef SecretKeySelector `json:"secretRef"`
}
type SecretKeySelector struct {
// The name of the Secret resource being referred to.
Name string `json:"name,omitempty"`
// The key where the token is found.
Key string `json:"key,omitempty"`
}
// Webhook connects to a third party API server to handle the secrets generation
// configuration parameters in spec.
// You can specify the server, the token, and additional body parameters.
// See documentation for the full API specification for requests and responses.
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced,categories={webhook},shortName=webhookl
type Webhook struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec WebhookSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// ExternalList contains a list of Webhook Generator resources.
type WebhookList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Webhook `json:"items"`
}

View file

@ -68,6 +68,14 @@ var (
PasswordGroupVersionKind = SchemeGroupVersion.WithKind(PasswordKind) PasswordGroupVersionKind = SchemeGroupVersion.WithKind(PasswordKind)
) )
// Webhook type metadata.
var (
WebhookKind = reflect.TypeOf(Webhook{}).Name()
WebhookGroupKind = schema.GroupKind{Group: Group, Kind: WebhookKind}.String()
WebhookKindAPIVersion = WebhookKind + "." + SchemeGroupVersion.String()
WebhookGroupVersionKind = SchemeGroupVersion.WithKind(WebhookKind)
)
// Fake type metadata. // Fake type metadata.
var ( var (
FakeKind = reflect.TypeOf(Fake{}).Name() FakeKind = reflect.TypeOf(Fake{}).Name()
@ -91,4 +99,5 @@ func init() {
SchemeBuilder.Register(&Fake{}, &FakeList{}) SchemeBuilder.Register(&Fake{}, &FakeList{})
SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{}) SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
SchemeBuilder.Register(&Password{}, &PasswordList{}) SchemeBuilder.Register(&Password{}, &PasswordList{})
SchemeBuilder.Register(&Webhook{}, &WebhookList{})
} }

View file

@ -22,6 +22,7 @@ import (
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/apis/meta/v1" "github.com/external-secrets/external-secrets/apis/meta/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
@ -653,6 +654,21 @@ func (in *PasswordSpec) DeepCopy() *PasswordSpec {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
if in == nil {
return nil
}
out := new(SecretKeySelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultDynamicSecret) DeepCopyInto(out *VaultDynamicSecret) { func (in *VaultDynamicSecret) DeepCopyInto(out *VaultDynamicSecret) {
*out = *in *out = *in
@ -735,3 +751,155 @@ func (in *VaultDynamicSecretSpec) DeepCopy() *VaultDynamicSecretSpec {
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Webhook) DeepCopyInto(out *Webhook) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Webhook.
func (in *Webhook) DeepCopy() *Webhook {
if in == nil {
return nil
}
out := new(Webhook)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Webhook) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookCAProvider) DeepCopyInto(out *WebhookCAProvider) {
*out = *in
if in.Namespace != nil {
in, out := &in.Namespace, &out.Namespace
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookCAProvider.
func (in *WebhookCAProvider) DeepCopy() *WebhookCAProvider {
if in == nil {
return nil
}
out := new(WebhookCAProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookList) DeepCopyInto(out *WebhookList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Webhook, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookList.
func (in *WebhookList) DeepCopy() *WebhookList {
if in == nil {
return nil
}
out := new(WebhookList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *WebhookList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookResult) DeepCopyInto(out *WebhookResult) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookResult.
func (in *WebhookResult) DeepCopy() *WebhookResult {
if in == nil {
return nil
}
out := new(WebhookResult)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookSecret) DeepCopyInto(out *WebhookSecret) {
*out = *in
out.SecretRef = in.SecretRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSecret.
func (in *WebhookSecret) DeepCopy() *WebhookSecret {
if in == nil {
return nil
}
out := new(WebhookSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookSpec) DeepCopyInto(out *WebhookSpec) {
*out = *in
if in.Headers != nil {
in, out := &in.Headers, &out.Headers
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout
*out = new(metav1.Duration)
**out = **in
}
out.Result = in.Result
if in.Secrets != nil {
in, out := &in.Secrets, &out.Secrets
*out = make([]WebhookSecret, len(*in))
copy(*out, *in)
}
if in.CABundle != nil {
in, out := &in.CABundle, &out.CABundle
*out = make([]byte, len(*in))
copy(*out, *in)
}
if in.CAProvider != nil {
in, out := &in.CAProvider, &out.CAProvider
*out = new(WebhookCAProvider)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSpec.
func (in *WebhookSpec) DeepCopy() *WebhookSpec {
if in == nil {
return nil
}
out := new(WebhookSpec)
in.DeepCopyInto(out)
return out
}

View file

@ -0,0 +1,141 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: webhooks.generators.external-secrets.io
spec:
group: generators.external-secrets.io
names:
categories:
- webhook
kind: Webhook
listKind: WebhookList
plural: webhooks
shortNames:
- webhookl
singular: webhook
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
Webhook connects to a third party API server to handle the secrets generation
configuration parameters in spec.
You can specify the server, the token, and additional body parameters.
See documentation for the full API specification for requests and responses.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: WebhookSpec controls the behavior of the external generator.
Any body parameters should be passed to the server through the parameters
field.
properties:
body:
description: Body
type: string
caBundle:
description: |-
PEM encoded CA bundle used to validate webhook server certificate. Only used
if the Server URL is using HTTPS protocol. This parameter is ignored for
plain HTTP protocol connection. If not set the system root certificates
are used to validate the TLS connection.
format: byte
type: string
caProvider:
description: The provider for the CA bundle to use to validate webhook
server certificate.
properties:
key:
description: The key the value inside of the provider type to
use, only used with "Secret" type
type: string
name:
description: The name of the object located at the provider type.
type: string
namespace:
description: The namespace the Provider type is in.
type: string
type:
description: The type of provider to use such as "Secret", or
"ConfigMap".
enum:
- Secret
- ConfigMap
type: string
required:
- name
- type
type: object
headers:
additionalProperties:
type: string
description: Headers
type: object
method:
description: Webhook Method
type: string
result:
description: Result formatting
properties:
jsonPath:
description: Json path of return value
type: string
type: object
secrets:
description: |-
Secrets to fill in templates
These secrets will be passed to the templating function as key value pairs under the given name
items:
properties:
name:
description: Name of this secret in templates
type: string
secretRef:
description: Secret ref to fill in credentials
properties:
key:
description: The key where the token is found.
type: string
name:
description: The name of the Secret resource being referred
to.
type: string
type: object
required:
- name
- secretRef
type: object
type: array
timeout:
description: Timeout
type: string
url:
description: Webhook url to call
type: string
required:
- result
- url
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -10910,3 +10910,149 @@ spec:
name: kubernetes name: kubernetes
namespace: default namespace: default
path: /convert path: /convert
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: webhooks.generators.external-secrets.io
spec:
group: generators.external-secrets.io
names:
categories:
- webhook
kind: Webhook
listKind: WebhookList
plural: webhooks
shortNames:
- webhookl
singular: webhook
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
Webhook connects to a third party API server to handle the secrets generation
configuration parameters in spec.
You can specify the server, the token, and additional body parameters.
See documentation for the full API specification for requests and responses.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: WebhookSpec controls the behavior of the external generator. Any body parameters should be passed to the server through the parameters field.
properties:
body:
description: Body
type: string
caBundle:
description: |-
PEM encoded CA bundle used to validate webhook server certificate. Only used
if the Server URL is using HTTPS protocol. This parameter is ignored for
plain HTTP protocol connection. If not set the system root certificates
are used to validate the TLS connection.
format: byte
type: string
caProvider:
description: The provider for the CA bundle to use to validate webhook server certificate.
properties:
key:
description: The key the value inside of the provider type to use, only used with "Secret" type
type: string
name:
description: The name of the object located at the provider type.
type: string
namespace:
description: The namespace the Provider type is in.
type: string
type:
description: The type of provider to use such as "Secret", or "ConfigMap".
enum:
- Secret
- ConfigMap
type: string
required:
- name
- type
type: object
headers:
additionalProperties:
type: string
description: Headers
type: object
method:
description: Webhook Method
type: string
result:
description: Result formatting
properties:
jsonPath:
description: Json path of return value
type: string
type: object
secrets:
description: |-
Secrets to fill in templates
These secrets will be passed to the templating function as key value pairs under the given name
items:
properties:
name:
description: Name of this secret in templates
type: string
secretRef:
description: Secret ref to fill in credentials
properties:
key:
description: The key where the token is found.
type: string
name:
description: The name of the Secret resource being referred to.
type: string
type: object
required:
- name
- secretRef
type: object
type: array
timeout:
description: Timeout
type: string
url:
description: Webhook url to call
type: string
required:
- result
- url
type: object
type: object
served: true
storage: true
subresources:
status: {}
conversion:
strategy: Webhook
webhook:
conversionReviewVersions:
- v1
clientConfig:
service:
name: kubernetes
namespace: default
path: /convert

View file

@ -0,0 +1,21 @@
The Webhook generator is very similar to SecretStore generator, and provides a way to use external systems to generate sensitive information.
## Output Keys and Values
Webhook calls are expected to produce valid JSON objects. All keys within that JSON object will be exported as keys to the kubernetes Secret.
## Example Manifest
```yaml
{% include 'generator-webhook.yaml' %}
```
Example `ExternalSecret` that references the Webhook generator using an internal `Secret`:
```yaml
{% include 'generator-webhook-example.yaml' %}
```
This will generate a kubernetes secret with the following values:
```yaml
parameter: test
```

View file

@ -123,3 +123,5 @@ spec:
key: <key inside secret> key: <key inside secret>
``` ```
### Webhook as generators
You can also leverage webhooks as generators, following the same syntax. The only difference is that the webhook generator needs its source secrets to be labeled, as opposed to webhook secretstores. Please see the [generator-webhook](../api/generator/webhook.md) documentation for more information.

View file

@ -0,0 +1,14 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: "webhook"
spec:
refreshInterval: "30m"
target:
name: webhook-secret
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Webhook
name: "webhook"

View file

@ -0,0 +1,28 @@
{% raw %}
apiVersion: generators.external-secrets.io/v1alpha1
kind: Webhook
metadata:
name: webhook
spec:
url: "http://httpbin.org/get?parameter={{ .auth.param }}"
result:
jsonPath: "$.args"
headers:
Content-Type: application/json
Authorization: Basic {{ print .auth.username ":" .auth.password | b64enc }}
secrets:
- name: auth
secretRef:
name: webhook-credentials
---
apiVersion: v1
kind: Secret
metadata:
name: webhook-credentials
labels:
generators.external-secrets.io/type: webhook #Needed to allow webhook to use this secret
data:
username: dGVzdA== # "test"
password: dGVzdA== # "test"
param: dGVzdA== # "test"
{% endraw %}

View file

@ -59,6 +59,7 @@ nav:
- Vault Dynamic Secret: api/generator/vault.md - Vault Dynamic Secret: api/generator/vault.md
- Password: api/generator/password.md - Password: api/generator/password.md
- Fake: api/generator/fake.md - Fake: api/generator/fake.md
- Webhook: api/generator/webhook.md
- Reference Docs: - Reference Docs:
- API specification: api/spec.md - API specification: api/spec.md
- Controller Options: api/controller-options.md - Controller Options: api/controller-options.md

View file

@ -0,0 +1,106 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Spec struct {
// Webhook Method
// +optional, default GET
Method string `json:"method,omitempty"`
// Webhook url to call
URL string `json:"url"`
// Headers
// +optional
Headers map[string]string `json:"headers,omitempty"`
// Body
// +optional
Body string `json:"body,omitempty"`
// Timeout
// +optional
Timeout *metav1.Duration `json:"timeout,omitempty"`
// Result formatting
Result Result `json:"result"`
// Secrets to fill in templates
// These secrets will be passed to the templating function as key value pairs under the given name
// +optional
Secrets []Secret `json:"secrets,omitempty"`
// PEM encoded CA bundle used to validate webhook server certificate. Only used
// if the Server URL is using HTTPS protocol. This parameter is ignored for
// plain HTTP protocol connection. If not set the system root certificates
// are used to validate the TLS connection.
// +optional
CABundle []byte `json:"caBundle,omitempty"`
// The provider for the CA bundle to use to validate webhook server certificate.
// +optional
CAProvider *CAProvider `json:"caProvider,omitempty"`
}
type CAProviderType string
const (
CAProviderTypeSecret CAProviderType = "Secret"
CAProviderTypeConfigMap CAProviderType = "ConfigMap"
)
// Defines a location to fetch the cert for the webhook provider from.
type CAProvider struct {
// The type of provider to use such as "Secret", or "ConfigMap".
// +kubebuilder:validation:Enum="Secret";"ConfigMap"
Type CAProviderType `json:"type"`
// The name of the object located at the provider type.
Name string `json:"name"`
// The key the value inside of the provider type to use, only used with "Secret" type
// +kubebuilder:validation:Optional
Key string `json:"key,omitempty"`
// The namespace the Provider type is in.
// +optional
Namespace *string `json:"namespace,omitempty"`
}
type Result struct {
// Json path of return value
// +optional
JSONPath string `json:"jsonPath,omitempty"`
}
type Secret struct {
// Name of this secret in templates
Name string `json:"name"`
// Secret ref to fill in credentials
SecretRef SecretKeySelector `json:"secretRef"`
}
type SecretKeySelector struct {
// The name of the Secret resource being referred to.
Name string `json:"name,omitempty"`
// The key where the token is found.
Key string `json:"key,omitempty"`
Namespace *string `json:"namespace,omitempty"`
}

View file

@ -0,0 +1,318 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
tpl "text/template"
"github.com/PaesslerAG/jsonpath"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/constants"
"github.com/external-secrets/external-secrets/pkg/metrics"
"github.com/external-secrets/external-secrets/pkg/template/v2"
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
)
type Webhook struct {
Kube client.Client
Namespace string
StoreKind string
HTTP *http.Client
EnforceLabels bool
ClusterScoped bool
}
func (w *Webhook) getStoreSecret(ctx context.Context, ref SecretKeySelector) (*corev1.Secret, error) {
ke := client.ObjectKey{
Name: ref.Name,
Namespace: w.Namespace,
}
if w.ClusterScoped {
if ref.Namespace == nil {
return nil, fmt.Errorf("no namespace on ClusterScoped webhook secret %s", ref.Name)
}
ke.Namespace = *ref.Namespace
}
secret := &corev1.Secret{}
if err := w.Kube.Get(ctx, ke, secret); err != nil {
return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
}
if w.EnforceLabels {
expected, ok := secret.Labels["generators.external-secrets.io/type"]
if !ok {
return nil, fmt.Errorf("secret does not contain needed label to be used on webhook generator")
}
if expected != "webhook" {
return nil, fmt.Errorf("secret type is not 'webhook'")
}
}
return secret, nil
}
func (w *Webhook) GetSecretMap(ctx context.Context, provider *Spec, ref *esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
result, err := w.GetWebhookData(ctx, provider, ref)
if err != nil {
return nil, err
}
// We always want json here, so just parse it out
jsondata := interface{}(nil)
if err := json.Unmarshal(result, &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse response json: %w", err)
}
// Get subdata via jsonpath, if given
if provider.Result.JSONPath != "" {
jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
if err != nil {
return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
}
}
// If the value is a string, try to parse it as json
jsonstring, ok := jsondata.(string)
if ok {
// This could also happen if the response was a single json-encoded string
// but that is an extremely unlikely scenario
if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
}
}
// Use the data as a key-value map
jsonvalue, ok := jsondata.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
}
// Change the map of generic objects to a map of byte arrays
values := make(map[string][]byte)
for rKey, rValue := range jsonvalue {
jVal, ok := rValue.(string)
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type in key '%s': %T)", rKey, rValue)
}
values[rKey] = []byte(jVal)
}
return values, nil
}
func (w *Webhook) GetTemplateData(ctx context.Context, ref *esv1beta1.ExternalSecretDataRemoteRef, secrets []Secret) (map[string]map[string]string, error) {
data := map[string]map[string]string{}
if ref != nil {
data["remoteRef"] = map[string]string{
"key": url.QueryEscape(ref.Key),
"version": url.QueryEscape(ref.Version),
"property": url.QueryEscape(ref.Property),
}
}
for _, secref := range secrets {
if _, ok := data[secref.Name]; !ok {
data[secref.Name] = make(map[string]string)
}
secret, err := w.getStoreSecret(ctx, secref.SecretRef)
if err != nil {
return nil, err
}
for sKey, sVal := range secret.Data {
data[secref.Name][sKey] = string(sVal)
}
}
return data, nil
}
func (w *Webhook) GetWebhookData(ctx context.Context, provider *Spec, ref *esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
if w.HTTP == nil {
return nil, fmt.Errorf("http client not initialized")
}
data, err := w.GetTemplateData(ctx, ref, provider.Secrets)
if err != nil {
return nil, err
}
method := provider.Method
if method == "" {
method = http.MethodGet
}
url, err := ExecuteTemplateString(provider.URL, data)
if err != nil {
return nil, fmt.Errorf("failed to parse url: %w", err)
}
body, err := ExecuteTemplate(provider.Body, data)
if err != nil {
return nil, fmt.Errorf("failed to parse body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, method, url, &body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
for hKey, hValueTpl := range provider.Headers {
hValue, err := ExecuteTemplateString(hValueTpl, data)
if err != nil {
return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
}
req.Header.Add(hKey, hValue)
}
resp, err := w.HTTP.Do(req)
metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
if err != nil {
return nil, fmt.Errorf("failed to call endpoint: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return nil, esv1beta1.NoSecretError{}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
}
return io.ReadAll(resp.Body)
}
func (w *Webhook) GetHTTPClient(provider *Spec) (*http.Client, error) {
client := &http.Client{}
if provider.Timeout != nil {
client.Timeout = provider.Timeout.Duration
}
if len(provider.CABundle) == 0 && provider.CAProvider == nil {
// No need to process ca stuff if it is not there
return client, nil
}
caCertPool, err := w.GetCACertPool(provider)
if err != nil {
return nil, err
}
tlsConf := &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
client.Transport = &http.Transport{TLSClientConfig: tlsConf}
return client, nil
}
func (w *Webhook) GetCACertPool(provider *Spec) (*x509.CertPool, error) {
caCertPool := x509.NewCertPool()
if len(provider.CABundle) > 0 {
ok := caCertPool.AppendCertsFromPEM(provider.CABundle)
if !ok {
return nil, fmt.Errorf("failed to append cabundle")
}
}
if provider.CAProvider != nil {
var cert []byte
var err error
switch provider.CAProvider.Type {
case CAProviderTypeSecret:
cert, err = w.GetCertFromSecret(provider)
case CAProviderTypeConfigMap:
cert, err = w.GetCertFromConfigMap(provider)
default:
err = fmt.Errorf("unknown caprovider type: %s", provider.CAProvider.Type)
}
if err != nil {
return nil, err
}
ok := caCertPool.AppendCertsFromPEM(cert)
if !ok {
return nil, fmt.Errorf("failed to append cabundle")
}
}
return caCertPool, nil
}
func (w *Webhook) GetCertFromSecret(provider *Spec) ([]byte, error) {
secretRef := esmeta.SecretKeySelector{
Name: provider.CAProvider.Name,
Namespace: &w.Namespace,
Key: provider.CAProvider.Key,
}
if provider.CAProvider.Namespace != nil {
secretRef.Namespace = provider.CAProvider.Namespace
}
ctx := context.Background()
cert, err := resolvers.SecretKeyRef(
ctx,
w.Kube,
w.StoreKind,
w.Namespace,
&secretRef,
)
if err != nil {
return nil, err
}
return []byte(cert), nil
}
func (w *Webhook) GetCertFromConfigMap(provider *Spec) ([]byte, error) {
objKey := client.ObjectKey{
Name: provider.CAProvider.Name,
}
if provider.CAProvider.Namespace != nil {
objKey.Namespace = *provider.CAProvider.Namespace
}
configMapRef := &corev1.ConfigMap{}
ctx := context.Background()
err := w.Kube.Get(ctx, objKey, configMapRef)
if err != nil {
return nil, fmt.Errorf("failed to get caprovider secret %s: %w", objKey.Name, err)
}
val, ok := configMapRef.Data[provider.CAProvider.Key]
if !ok {
return nil, fmt.Errorf("failed to get caprovider configmap %s -> %s", objKey.Name, provider.CAProvider.Key)
}
return []byte(val), nil
}
func ExecuteTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
result, err := ExecuteTemplate(tmpl, data)
if err != nil {
return "", err
}
return result.String(), nil
}
func ExecuteTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
var result bytes.Buffer
if tmpl == "" {
return result, nil
}
urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
if err != nil {
return result, err
}
if err := urlt.Execute(&result, data); err != nil {
return result, err
}
return result, nil
}

View file

@ -24,4 +24,5 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/generator/gcr" _ "github.com/external-secrets/external-secrets/pkg/generator/gcr"
_ "github.com/external-secrets/external-secrets/pkg/generator/password" _ "github.com/external-secrets/external-secrets/pkg/generator/password"
_ "github.com/external-secrets/external-secrets/pkg/generator/vault" _ "github.com/external-secrets/external-secrets/pkg/generator/vault"
_ "github.com/external-secrets/external-secrets/pkg/generator/webhook"
) )

View file

@ -0,0 +1,72 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"context"
"encoding/json"
"fmt"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/common/webhook"
)
type Webhook struct {
wh webhook.Webhook
url string
}
func (w *Webhook) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kclient client.Client, ns string) (map[string][]byte, error) {
w.wh.EnforceLabels = true
w.wh.ClusterScoped = false
provider, err := parseSpec(jsonSpec.Raw)
w.wh = webhook.Webhook{}
if err != nil {
return nil, err
}
w.wh.Namespace = ns
w.url = provider.URL
w.wh.Kube = kclient
w.wh.HTTP, err = w.wh.GetHTTPClient(provider)
if err != nil {
return nil, err
}
if err != nil {
return nil, fmt.Errorf("failed to get store: %w", err)
}
return w.wh.GetSecretMap(ctx, provider, nil)
}
func parseSpec(data []byte) (*webhook.Spec, error) {
var spec genv1alpha1.Webhook
err := json.Unmarshal(data, &spec)
if err != nil {
return nil, err
}
out := webhook.Spec{}
d, err := json.Marshal(spec.Spec)
if err != nil {
return nil, err
}
err = json.Unmarshal(d, &out)
return &out, err
}
func init() {
genv1alpha1.Register(genv1alpha1.WebhookKind, &Webhook{})
}

View file

@ -0,0 +1,266 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"gopkg.in/yaml.v3"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
)
type testCase struct {
Case string `json:"case,omitempty"`
Args args `json:"args"`
Want want `json:"want"`
}
type args struct {
URL string `json:"url,omitempty"`
Body string `json:"body,omitempty"`
Timeout string `json:"timeout,omitempty"`
Key string `json:"key,omitempty"`
Property string `json:"property,omitempty"`
Version string `json:"version,omitempty"`
JSONPath string `json:"jsonpath,omitempty"`
Response string `json:"response,omitempty"`
StatusCode int `json:"statuscode,omitempty"`
}
type want struct {
Path string `json:"path,omitempty"`
Err string `json:"err,omitempty"`
Result string `json:"result,omitempty"`
ResultMap map[string]string `json:"resultmap,omitempty"`
}
var testCases = `
case: error url
args:
url: /api/getsecret?id={{ .unclosed.template
want:
err: failed to parse url
---
case: error body
args:
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
body: Body error {{ .unclosed.template
want:
err: failed to parse body
---
case: error connection
args:
url: 1/api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
want:
err: failed to call endpoint
---
case: error no secret err
args:
url: /api/getsecret?id=testkey&version=1
key: testkey
version: 1
statuscode: 404
response: not found
want:
path: /api/getsecret?id=testkey&version=1
err: ` + esv1beta1.NoSecretErr.Error() + `
---
case: error server error
args:
url: /api/getsecret?id=testkey&version=1
key: testkey
version: 1
statuscode: 500
response: server error
want:
path: /api/getsecret?id=testkey&version=1
err: endpoint gave error 500
---
case: error bad json
args:
url: /api/getsecret?id=testkey&version=1
key: testkey
version: 1
jsonpath: $.result.thesecret
response: '{"result":{"thesecret":"secret-value"}'
want:
path: /api/getsecret?id=testkey&version=1
err: failed to parse response json
---
case: error bad jsonpath
args:
url: /api/getsecret?id=testkey&version=1
key: testkey
version: 1
jsonpath: $.result.thesecret
response: '{"result":{"nosecret":"secret-value"}}'
want:
path: /api/getsecret?id=testkey&version=1
err: failed to get response path
---
case: pull data out of map
args:
url: /api/getsecret?id=testkey&version=1
key: testkey
version: 1
jsonpath: $.result.thesecret
response: '{"result":{"thesecret":{"one":"secret-value"}}}'
want:
path: /api/getsecret?id=testkey&version=1
err: ''
result: '{"one":"secret-value"}'
---
case: not valid response path
args:
url: /api/getsecret?id=testkey&version=1
key: testkey
version: 1
jsonpath: $.result.unexisting
response: '{"result":{"thesecret":{"one":"secret-value"}}}'
want:
path: /api/getsecret?id=testkey&version=1
err: 'failed to get response path'
result: ''
---
case: response path not json
args:
url: /api/getsecret?id=testkey&version=1
key: testkey
version: 1
jsonpath: $.result.thesecret
response: '{"result":{"thesecret":[{"one":"secret-value"}]}}'
want:
path: /api/getsecret?id=testkey&version=1
err: 'failed to get response (wrong type:'
result: ''
`
func TestWebhookGetSecret(t *testing.T) {
ydec := yaml.NewDecoder(bytes.NewReader([]byte(testCases)))
for {
var tc testCase
if err := ydec.Decode(&tc); err != nil {
if !errors.Is(err, io.EOF) {
t.Errorf("testcase decode error %v", err)
}
break
}
runTestCase(tc, t)
}
}
func testCaseServer(tc testCase, t *testing.T) *httptest.Server {
// Start a new server for every test case because the server wants to check the expected api path
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if tc.Want.Path != "" && req.URL.String() != tc.Want.Path {
t.Errorf("%s: unexpected api path: %s, expected %s", tc.Case, req.URL.String(), tc.Want.Path)
}
if tc.Args.StatusCode != 0 {
rw.WriteHeader(tc.Args.StatusCode)
}
rw.Write([]byte(tc.Args.Response))
}))
}
func parseTimeout(timeout string) (*metav1.Duration, error) {
if timeout == "" {
return nil, nil
}
dur, err := time.ParseDuration(timeout)
if err != nil {
return nil, err
}
return &metav1.Duration{Duration: dur}, nil
}
func runTestCase(tc testCase, t *testing.T) {
ts := testCaseServer(tc, t)
defer ts.Close()
testStore := makeGenerator(ts.URL, tc.Args)
jsonRes, err := json.Marshal(testStore)
if err != nil {
t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
return
}
genSpec := &apiextensions.JSON{Raw: jsonRes}
timeout, err := parseTimeout(tc.Args.Timeout)
if err != nil {
t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
return
}
testStore.Spec.Timeout = timeout
testProv := &Webhook{}
testGenerate(tc, t, testProv, genSpec)
}
func testGenerate(tc testCase, t *testing.T, client genv1alpha1.Generator, testStore *apiextensions.JSON) {
secretmap, err := client.Generate(context.Background(), testStore, nil, "testnamespace")
errStr := ""
if err != nil {
errStr = err.Error()
}
if (tc.Want.Err == "") != (errStr == "") || !strings.Contains(errStr, tc.Want.Err) {
t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
}
if err == nil {
for wantkey, wantval := range tc.Want.ResultMap {
gotval, ok := secretmap[wantkey]
if !ok {
t.Errorf("%s: unexpected response: wanted key '%s' not found", tc.Case, wantkey)
} else if string(gotval) != wantval {
t.Errorf("%s: unexpected response: key '%s' = '%s' (expected '%s')", tc.Case, wantkey, wantval, gotval)
}
}
}
}
func makeGenerator(url string, args args) *genv1alpha1.Webhook {
store := &genv1alpha1.Webhook{
TypeMeta: metav1.TypeMeta{
Kind: "Webhook",
},
ObjectMeta: metav1.ObjectMeta{
Name: "wehbook-store",
Namespace: "default",
},
Spec: genv1alpha1.WebhookSpec{
URL: url + args.URL,
Body: args.Body,
Headers: map[string]string{
"Content-Type": "application.json",
"X-SecretKey": "{{ .remoteRef.key }}",
},
Result: genv1alpha1.WebhookResult{
JSONPath: args.JSONPath,
},
},
}
return store
}

View file

@ -15,17 +15,10 @@ limitations under the License.
package webhook package webhook
import ( import (
"bytes"
"context" "context"
"crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http"
"net/url"
"strconv" "strconv"
tpl "text/template"
"time" "time"
"github.com/PaesslerAG/jsonpath" "github.com/PaesslerAG/jsonpath"
@ -34,12 +27,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" "github.com/external-secrets/external-secrets/pkg/common/webhook"
"github.com/external-secrets/external-secrets/pkg/constants"
"github.com/external-secrets/external-secrets/pkg/metrics"
"github.com/external-secrets/external-secrets/pkg/template/v2"
"github.com/external-secrets/external-secrets/pkg/utils" "github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
) )
// https://github.com/external-secrets/external-secrets/issues/644 // https://github.com/external-secrets/external-secrets/issues/644
@ -50,11 +39,9 @@ var _ esv1beta1.Provider = &Provider{}
type Provider struct{} type Provider struct{}
type WebHook struct { type WebHook struct {
kube client.Client wh webhook.Webhook
store esv1beta1.GenericStore store esv1beta1.GenericStore
namespace string
storeKind string storeKind string
http *http.Client
url string url string
} }
@ -70,19 +57,25 @@ func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
} }
func (p *Provider) NewClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) { func (p *Provider) NewClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
wh := webhook.Webhook{
Kube: kube,
Namespace: namespace,
}
whClient := &WebHook{ whClient := &WebHook{
kube: kube,
store: store, store: store,
namespace: namespace, wh: wh,
storeKind: store.GetObjectKind().GroupVersionKind().Kind, storeKind: store.GetObjectKind().GroupVersionKind().Kind,
} }
if whClient.storeKind == esv1beta1.ClusterSecretStoreKind {
whClient.wh.ClusterScoped = true
}
provider, err := getProvider(store) provider, err := getProvider(store)
if err != nil { if err != nil {
return nil, err return nil, err
} }
whClient.url = provider.URL whClient.url = provider.URL
whClient.http, err = whClient.getHTTPClient(provider) whClient.wh.HTTP, err = whClient.wh.GetHTTPClient(provider)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -93,30 +86,18 @@ func (p *Provider) ValidateStore(_ esv1beta1.GenericStore) (admission.Warnings,
return nil, nil return nil, nil
} }
func getProvider(store esv1beta1.GenericStore) (*esv1beta1.WebhookProvider, error) { func getProvider(store esv1beta1.GenericStore) (*webhook.Spec, error) {
spc := store.GetSpec() spc := store.GetSpec()
if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil { if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {
return nil, fmt.Errorf("missing store provider webhook") return nil, fmt.Errorf("missing store provider webhook")
} }
return spc.Provider.Webhook, nil out := webhook.Spec{}
} d, err := json.Marshal(spc.Provider.Webhook)
if err != nil {
func (w *WebHook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelector) (*corev1.Secret, error) { return nil, err
ke := client.ObjectKey{
Name: ref.Name,
Namespace: w.namespace,
} }
if w.storeKind == esv1beta1.ClusterSecretStoreKind { err = json.Unmarshal(d, &out)
if ref.Namespace == nil { return &out, err
return nil, fmt.Errorf("no namespace on ClusterSecretStore webhook secret %s", ref.Name)
}
ke.Namespace = *ref.Namespace
}
secret := &corev1.Secret{}
if err := w.kube.Get(ctx, ke, secret); err != nil {
return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
}
return secret, nil
} }
func (w *WebHook) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error { func (w *WebHook) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
@ -139,16 +120,16 @@ func (w *WebHook) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDat
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get store: %w", err) return nil, fmt.Errorf("failed to get store: %w", err)
} }
result, err := w.getWebhookData(ctx, provider, ref) result, err := w.wh.GetWebhookData(ctx, provider, &ref)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Only parse as json if we have a jsonpath set // Only parse as json if we have a jsonpath set
data, err := w.getTemplateData(ctx, ref, provider.Secrets) data, err := w.wh.GetTemplateData(ctx, &ref, provider.Secrets)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resultJSONPath, err := executeTemplateString(provider.Result.JSONPath, data) resultJSONPath, err := webhook.ExecuteTemplateString(provider.Result.JSONPath, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -207,229 +188,7 @@ func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecret
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get store: %w", err) return nil, fmt.Errorf("failed to get store: %w", err)
} }
result, err := w.getWebhookData(ctx, provider, ref) return w.wh.GetSecretMap(ctx, provider, &ref)
if err != nil {
return nil, err
}
// We always want json here, so just parse it out
jsondata := interface{}(nil)
if err := json.Unmarshal(result, &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse response json: %w", err)
}
// Get subdata via jsonpath, if given
if provider.Result.JSONPath != "" {
jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
if err != nil {
return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
}
}
// If the value is a string, try to parse it as json
jsonstring, ok := jsondata.(string)
if ok {
// This could also happen if the response was a single json-encoded string
// but that is an extremely unlikely scenario
if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
}
}
// Use the data as a key-value map
jsonvalue, ok := jsondata.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
}
// Change the map of generic objects to a map of byte arrays
values := make(map[string][]byte)
for rKey, rValue := range jsonvalue {
jVal, ok := rValue.(string)
if !ok {
return nil, fmt.Errorf("failed to get response (wrong type in key '%s': %T)", rKey, rValue)
}
values[rKey] = []byte(jVal)
}
return values, nil
}
func (w *WebHook) getTemplateData(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef, secrets []esv1beta1.WebhookSecret) (map[string]map[string]string, error) {
data := map[string]map[string]string{
"remoteRef": {
"key": url.QueryEscape(ref.Key),
"version": url.QueryEscape(ref.Version),
"property": url.QueryEscape(ref.Property),
},
}
for _, secref := range secrets {
if _, ok := data[secref.Name]; !ok {
data[secref.Name] = make(map[string]string)
}
secret, err := w.getStoreSecret(ctx, secref.SecretRef)
if err != nil {
return nil, err
}
for sKey, sVal := range secret.Data {
data[secref.Name][sKey] = string(sVal)
}
}
return data, nil
}
func (w *WebHook) getWebhookData(ctx context.Context, provider *esv1beta1.WebhookProvider, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
if w.http == nil {
return nil, fmt.Errorf("http client not initialized")
}
data, err := w.getTemplateData(ctx, ref, provider.Secrets)
if err != nil {
return nil, err
}
method := provider.Method
if method == "" {
method = http.MethodGet
}
url, err := executeTemplateString(provider.URL, data)
if err != nil {
return nil, fmt.Errorf("failed to parse url: %w", err)
}
body, err := executeTemplate(provider.Body, data)
if err != nil {
return nil, fmt.Errorf("failed to parse body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, method, url, &body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
for hKey, hValueTpl := range provider.Headers {
hValue, err := executeTemplateString(hValueTpl, data)
if err != nil {
return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
}
req.Header.Add(hKey, hValue)
}
resp, err := w.http.Do(req)
metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
if err != nil {
return nil, fmt.Errorf("failed to call endpoint: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return nil, esv1beta1.NoSecretError{}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
}
return io.ReadAll(resp.Body)
}
func (w *WebHook) getHTTPClient(provider *esv1beta1.WebhookProvider) (*http.Client, error) {
client := &http.Client{}
if provider.Timeout != nil {
client.Timeout = provider.Timeout.Duration
}
if len(provider.CABundle) == 0 && provider.CAProvider == nil {
// No need to process ca stuff if it is not there
return client, nil
}
caCertPool, err := w.getCACertPool(provider)
if err != nil {
return nil, err
}
tlsConf := &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
client.Transport = &http.Transport{TLSClientConfig: tlsConf}
return client, nil
}
func (w *WebHook) getCACertPool(provider *esv1beta1.WebhookProvider) (*x509.CertPool, error) {
caCertPool := x509.NewCertPool()
if len(provider.CABundle) > 0 {
ok := caCertPool.AppendCertsFromPEM(provider.CABundle)
if !ok {
return nil, fmt.Errorf("failed to append cabundle")
}
}
if provider.CAProvider != nil && w.storeKind == esv1beta1.ClusterSecretStoreKind && provider.CAProvider.Namespace == nil {
return nil, fmt.Errorf("missing namespace on CAProvider secret")
}
if provider.CAProvider != nil {
var cert []byte
var err error
switch provider.CAProvider.Type {
case esv1beta1.WebhookCAProviderTypeSecret:
cert, err = w.getCertFromSecret(provider)
case esv1beta1.WebhookCAProviderTypeConfigMap:
cert, err = w.getCertFromConfigMap(provider)
default:
err = fmt.Errorf("unknown caprovider type: %s", provider.CAProvider.Type)
}
if err != nil {
return nil, err
}
ok := caCertPool.AppendCertsFromPEM(cert)
if !ok {
return nil, fmt.Errorf("failed to append cabundle")
}
}
return caCertPool, nil
}
func (w *WebHook) getCertFromSecret(provider *esv1beta1.WebhookProvider) ([]byte, error) {
secretRef := esmeta.SecretKeySelector{
Name: provider.CAProvider.Name,
Namespace: &w.namespace,
Key: provider.CAProvider.Key,
}
if provider.CAProvider.Namespace != nil {
secretRef.Namespace = provider.CAProvider.Namespace
}
ctx := context.Background()
cert, err := resolvers.SecretKeyRef(
ctx,
w.kube,
w.storeKind,
w.namespace,
&secretRef,
)
if err != nil {
return nil, err
}
return []byte(cert), nil
}
func (w *WebHook) getCertFromConfigMap(provider *esv1beta1.WebhookProvider) ([]byte, error) {
objKey := client.ObjectKey{
Name: provider.CAProvider.Name,
}
if provider.CAProvider.Namespace != nil {
objKey.Namespace = *provider.CAProvider.Namespace
}
configMapRef := &corev1.ConfigMap{}
ctx := context.Background()
err := w.kube.Get(ctx, objKey, configMapRef)
if err != nil {
return nil, fmt.Errorf("failed to get caprovider secret %s: %w", objKey.Name, err)
}
val, ok := configMapRef.Data[provider.CAProvider.Key]
if !ok {
return nil, fmt.Errorf("failed to get caprovider configmap %s -> %s", objKey.Name, provider.CAProvider.Key)
}
return []byte(val), nil
} }
func (w *WebHook) Close(_ context.Context) error { func (w *WebHook) Close(_ context.Context) error {
@ -445,26 +204,3 @@ func (w *WebHook) Validate() (esv1beta1.ValidationResult, error) {
} }
return esv1beta1.ValidationResultReady, nil return esv1beta1.ValidationResultReady, nil
} }
func executeTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
result, err := executeTemplate(tmpl, data)
if err != nil {
return "", err
}
return result.String(), nil
}
func executeTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
var result bytes.Buffer
if tmpl == "" {
return result, nil
}
urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
if err != nil {
return result, err
}
if err := urlt.Execute(&result, data); err != nil {
return result, err
}
return result, nil
}