diff --git a/apis/externalsecrets/v1beta1/secretstore_chef_types.go b/apis/externalsecrets/v1beta1/secretstore_chef_types.go
new file mode 100644
index 00000000000..136b0fac67a
--- /dev/null
+++ b/apis/externalsecrets/v1beta1/secretstore_chef_types.go
@@ -0,0 +1,38 @@
+/*
+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 v1beta1
+
+import (
+ esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// ChefAuth contains a secretRef for credentials.
+type ChefAuth struct {
+ SecretRef ChefAuthSecretRef `json:"secretRef"`
+}
+
+// ChefAuthSecretRef holds secret references for chef server login credentials.
+type ChefAuthSecretRef struct {
+ // SecretKey is the Signing Key in PEM format, used for authentication.
+ SecretKey esmeta.SecretKeySelector `json:"privateKeySecretRef"`
+}
+
+// ChefProvider configures a store to sync secrets using basic chef server connection credentials.
+type ChefProvider struct {
+ // Auth defines the information necessary to authenticate against chef Server
+ Auth *ChefAuth `json:"auth"`
+ // UserName should be the user ID on the chef server
+ UserName string `json:"username"`
+ // ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a "/"
+ ServerURL string `json:"serverUrl"`
+}
diff --git a/apis/externalsecrets/v1beta1/secretstore_types.go b/apis/externalsecrets/v1beta1/secretstore_types.go
index e818298a187..42edc797880 100644
--- a/apis/externalsecrets/v1beta1/secretstore_types.go
+++ b/apis/externalsecrets/v1beta1/secretstore_types.go
@@ -141,6 +141,10 @@ type SecretStoreProvider struct {
// https://docs.delinea.com/online-help/products/devops-secrets-vault/current
// +optional
Delinea *DelineaProvider `json:"delinea,omitempty"`
+
+ // Chef configures this store to sync secrets with chef server
+ // +optional
+ Chef *ChefProvider `json:"chef,omitempty"`
}
type CAProviderType string
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index 87abd5d5ce5..e748363897f 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
@@ -418,6 +418,58 @@ func (in *CertAuth) DeepCopy() *CertAuth {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ChefAuth) DeepCopyInto(out *ChefAuth) {
+ *out = *in
+ in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChefAuth.
+func (in *ChefAuth) DeepCopy() *ChefAuth {
+ if in == nil {
+ return nil
+ }
+ out := new(ChefAuth)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ChefAuthSecretRef) DeepCopyInto(out *ChefAuthSecretRef) {
+ *out = *in
+ in.SecretKey.DeepCopyInto(&out.SecretKey)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChefAuthSecretRef.
+func (in *ChefAuthSecretRef) DeepCopy() *ChefAuthSecretRef {
+ if in == nil {
+ return nil
+ }
+ out := new(ChefAuthSecretRef)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ChefProvider) DeepCopyInto(out *ChefProvider) {
+ *out = *in
+ if in.Auth != nil {
+ in, out := &in.Auth, &out.Auth
+ *out = new(ChefAuth)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChefProvider.
+func (in *ChefProvider) DeepCopy() *ChefProvider {
+ if in == nil {
+ return nil
+ }
+ out := new(ChefProvider)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterExternalSecret) DeepCopyInto(out *ClusterExternalSecret) {
*out = *in
@@ -1992,6 +2044,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(DelineaProvider)
(*in).DeepCopyInto(*out)
}
+ if in.Chef != nil {
+ in, out := &in.Chef, &out.Chef
+ *out = new(ChefProvider)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
index a1bbe6aabfa..81522c14930 100644
--- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml
+++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
@@ -2202,6 +2202,56 @@ spec:
required:
- vaultUrl
type: object
+ chef:
+ description: Chef configures this store to sync secrets with chef
+ server
+ properties:
+ auth:
+ description: Auth defines the information necessary to authenticate
+ against chef Server
+ properties:
+ secretRef:
+ description: ChefAuthSecretRef holds secret references
+ for chef server login credentials.
+ properties:
+ privateKeySecretRef:
+ description: SecretKey is the Signing Key in PEM format,
+ used for authentication.
+ 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:
+ - privateKeySecretRef
+ type: object
+ required:
+ - secretRef
+ type: object
+ serverUrl:
+ description: ServerURL is the chef server URL used to connect
+ to. If using orgs you should include your org in the url
+ and terminate the url with a "/"
+ type: string
+ username:
+ description: UserName should be the user ID on the chef server
+ type: string
+ required:
+ - auth
+ - serverUrl
+ - username
+ type: object
conjur:
description: Conjur configures this store to sync secrets using
conjur provider
diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml
index f92a45c2c80..8b034fcefa4 100644
--- a/config/crds/bases/external-secrets.io_secretstores.yaml
+++ b/config/crds/bases/external-secrets.io_secretstores.yaml
@@ -2202,6 +2202,56 @@ spec:
required:
- vaultUrl
type: object
+ chef:
+ description: Chef configures this store to sync secrets with chef
+ server
+ properties:
+ auth:
+ description: Auth defines the information necessary to authenticate
+ against chef Server
+ properties:
+ secretRef:
+ description: ChefAuthSecretRef holds secret references
+ for chef server login credentials.
+ properties:
+ privateKeySecretRef:
+ description: SecretKey is the Signing Key in PEM format,
+ used for authentication.
+ 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:
+ - privateKeySecretRef
+ type: object
+ required:
+ - secretRef
+ type: object
+ serverUrl:
+ description: ServerURL is the chef server URL used to connect
+ to. If using orgs you should include your org in the url
+ and terminate the url with a "/"
+ type: string
+ username:
+ description: UserName should be the user ID on the chef server
+ type: string
+ required:
+ - auth
+ - serverUrl
+ - username
+ type: object
conjur:
description: Conjur configures this store to sync secrets using
conjur provider
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index 57e629f18ef..e10b8a0fc64 100644
--- a/deploy/crds/bundle.yaml
+++ b/deploy/crds/bundle.yaml
@@ -2665,6 +2665,49 @@ spec:
required:
- vaultUrl
type: object
+ chef:
+ description: Chef configures this store to sync secrets with chef server
+ properties:
+ auth:
+ description: Auth defines the information necessary to authenticate against chef Server
+ properties:
+ secretRef:
+ description: ChefAuthSecretRef holds secret references for chef server login credentials.
+ properties:
+ privateKeySecretRef:
+ description: SecretKey is the Signing Key in PEM format, used for authentication.
+ 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:
+ - privateKeySecretRef
+ type: object
+ required:
+ - secretRef
+ type: object
+ serverUrl:
+ description: ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a "/"
+ type: string
+ username:
+ description: UserName should be the user ID on the chef server
+ type: string
+ required:
+ - auth
+ - serverUrl
+ - username
+ type: object
conjur:
description: Conjur configures this store to sync secrets using conjur provider
properties:
@@ -7639,6 +7682,49 @@ spec:
required:
- vaultUrl
type: object
+ chef:
+ description: Chef configures this store to sync secrets with chef server
+ properties:
+ auth:
+ description: Auth defines the information necessary to authenticate against chef Server
+ properties:
+ secretRef:
+ description: ChefAuthSecretRef holds secret references for chef server login credentials.
+ properties:
+ privateKeySecretRef:
+ description: SecretKey is the Signing Key in PEM format, used for authentication.
+ 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:
+ - privateKeySecretRef
+ type: object
+ required:
+ - secretRef
+ type: object
+ serverUrl:
+ description: ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a "/"
+ type: string
+ username:
+ description: UserName should be the user ID on the chef server
+ type: string
+ required:
+ - auth
+ - serverUrl
+ - username
+ type: object
conjur:
description: Conjur configures this store to sync secrets using conjur provider
properties:
diff --git a/docs/api/spec.md b/docs/api/spec.md
index 479819bbb49..ac64bbdb0ca 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -1108,6 +1108,123 @@ External Secrets meta/v1.SecretKeySelector
+
ChefAuth
+
+
+(Appears on:
+ChefProvider)
+
+
+
ChefAuth contains a secretRef for credentials.
+
+
+ChefAuthSecretRef
+
+
+(Appears on:
+ChefAuth)
+
+
+
ChefAuthSecretRef holds secret references for chef server login credentials.
+
+
+ChefProvider
+
+
+(Appears on:
+SecretStoreProvider)
+
+
+
ChefProvider configures a store to sync secrets using basic chef server connection credentials.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+auth
+
+
+ChefAuth
+
+
+ |
+
+ Auth defines the information necessary to authenticate against chef Server
+ |
+
+
+
+username
+
+string
+
+ |
+
+ UserName should be the user ID on the chef server
+ |
+
+
+
+serverUrl
+
+string
+
+ |
+
+ ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a “/”
+ |
+
+
+
ClusterExternalSecret
@@ -5289,6 +5406,20 @@ DelineaProvider
https://docs.delinea.com/online-help/products/devops-secrets-vault/current
+
+
+chef
+
+
+ChefProvider
+
+
+ |
+
+(Optional)
+ Chef configures this store to sync secrets with chef server
+ |
+
SecretStoreRef
diff --git a/docs/provider/chef.md b/docs/provider/chef.md
new file mode 100644
index 00000000000..51622bbed8a
--- /dev/null
+++ b/docs/provider/chef.md
@@ -0,0 +1,113 @@
+## Chef
+
+`Chef External Secrets provider` will enable users to seamlessly integrate their Chef-based secret management with Kubernetes through the existing External Secrets framework.
+
+In many enterprises, legacy applications and infrastructure are still tightly integrated with the Chef/Chef Infra Server/Chef Server Cluster for configuration and secrets management. Teams often rely on [Chef data bags](https://docs.chef.io/data_bags/) to securely store sensitive information such as application secrets and infrastructure configurations. These data bags serve as a centralized repository for managing and distributing sensitive data across the Chef ecosystem.
+
+**NOTE:** `Chef External Secrets provider` is designed only to fetch data from the Chef data bags into Kubernetes secrets, it won't update/delete any item in the data bags.
+
+### Authentication
+
+Every request made to the Chef Infra server needs to be authenticated. [Authentication](https://docs.chef.io/server/auth/) is done using the Private keys of the Chef Users. The User needs to have appropriate [Permissions](https://docs.chef.io/server/server_orgs/#permissions) to the data bags containing the data that they want to fetch using the External Secrets Operator.
+
+The following command can be used to create Chef Users:
+```sh
+chef-server-ctl user-create USER_NAME FIRST_NAME [MIDDLE_NAME] LAST_NAME EMAIL 'PASSWORD' (options)
+```
+
+More details on the above command are available here [Chef User Create Option](https://docs.chef.io/server/server_users/#user-create). The above command will return the default private key (PRIVATE_KEY_VALUE), which we will use for authentication. Additionally, a Chef User with access to specific data bags, a private key pair with an expiration date can be created with the help of the [knife user key](https://docs.chef.io/server/auth/#knife-user-key) command.
+
+### Create a secret containing your private key
+
+We need to store the above User's API key into a secret resource.
+Example:
+```sh
+kubectl create secret generic chef-user-secret -n vivid --from-literal=user-private-key='PRIVATE_KEY_VALUE'
+```
+
+### Creating ClusterSecretStore
+
+The Chef `ClusterSecretStore` is a cluster-scoped SecretStore that can be referenced by all Chef `ExternalSecrets` from all namespaces. You can follow the below example to create a `ClusterSecretStore` resource.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ClusterSecretStore
+metadata:
+ name: vivid-clustersecretstore # name of ClusterSecretStore
+spec:
+ provider:
+ chef:
+ username: user # Chef User name
+ serverUrl: https://manage.chef.io/organizations/testuser/ # Chef server URL
+ auth:
+ secretRef:
+ privateKeySecretRef:
+ key: user-private-key # name of the key inside Secret resource
+ name: chef-user-secret # name of Kubernetes Secret resource containing the Chef User's private key
+ namespace: vivid # the namespace in which the above Secret resource resides
+```
+
+### Creating SecretStore
+
+Chef `SecretStores` are bound to a namespace and can not reference resources across namespaces. For cross-namespace SecretStores, you must use Chef `ClusterSecretStores`.
+
+You can follow the below example to create a `SecretStore` resource.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+ name: vivid-secretstore # name of SecretStore
+ namespace: vivid # must be required for kind: SecretStore
+spec:
+ provider:
+ chef:
+ username: user # Chef User name
+ serverUrl: https://manage.chef.io/organizations/testuser/ # Chef server URL
+ auth:
+ secretRef:
+ privateKeySecretRef:
+ name: chef-user-secret # name of Kubernetes Secret resource containing the Chef User's private key
+ key: user-private-key # name of the key inside Secret resource
+ namespace: vivid # the ns where the k8s secret resource containing Chef User's private key resides
+
+```
+
+### Creating ExternalSecret
+
+The Chef `ExternalSecret` describes what data should be fetched from Chef Data bags, and how the data should be transformed and saved as a Kind=Secret.
+
+You can follow the below example to create an `ExternalSecret` resource.
+```yaml
+{% include 'chef-external-secret.yaml' %}
+```
+
+When the above `ClusterSecretStore` and `ExternalSecret` resources are created, the `ExternalSecret` will connect to the Chef Server using the private key and will fetch the data bags contained in the `vivid-credentials` secret resource.
+
+To get all data items inside the data bag, you can use the `dataFrom` directive:
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: vivid-external-secrets # name of ExternalSecret
+ namespace: vivid # namespace inside which the ExternalSecret will be created
+ annotations:
+ company/contacts: user.a@company.com, user.b@company.com
+ company/team: vivid-dev
+ labels:
+ app.kubernetes.io/name: external-secrets
+spec:
+ refreshInterval: 15m
+ secretStoreRef:
+ name: vivid-clustersecretstore # name of ClusterSecretStore
+ kind: ClusterSecretStore
+ dataFrom:
+ - extract:
+ key: vivid_global # only data bag name
+ target:
+ name: vivid_global_all_cred # name of Kubernetes Secret resource that will be created and will contain the obtained secrets
+ creationPolicy: Owner
+
+```
+
+follow : [this file](https://github.com/external-secrets/external-secrets/blob/main/apis/externalsecrets/v1beta1/secretstore_chef_types.go) for more info
diff --git a/docs/snippets/chef-external-secret.yaml b/docs/snippets/chef-external-secret.yaml
new file mode 100644
index 00000000000..704a6bb9f48
--- /dev/null
+++ b/docs/snippets/chef-external-secret.yaml
@@ -0,0 +1,48 @@
+{% raw %}
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: vivid-external-secrets # name of ExternalSecret
+ namespace: vivid # namespace inside which the ExternalSecret will be created
+ annotations:
+ company/contacts: user.a@company.com, user.b@company.com
+ company/team: vivid-dev
+ labels:
+ app.kubernetes.io/name: external-secrets
+spec:
+ refreshInterval: 15m
+ secretStoreRef:
+ name: vivid-clustersecretstore # name of ClusterSecretStore
+ kind: ClusterSecretStore
+ data:
+ - secretKey: USERNAME
+ remoteRef:
+ key: vivid_prod/global_user # databagName/dataItemName
+ property: username # a json key in dataItem
+ - secretKey: PASSWORD
+ remoteRef:
+ key: vivid_prod/global_user
+ property: password
+ - secretKey: APIKEY
+ remoteRef:
+ key: vivid_global/apikey
+ property: api_key
+ - secretKey: APP_PROPERTIES
+ remoteRef:
+ key: vivid_global/app_properties # databagName/dataItemName , it will fetch all key-vlaues present in the dataItem
+ target:
+ name: vivid-credentials # name of kubernetes Secret resource that will be created and will contain the obtained secrets
+ creationPolicy: Owner
+ template:
+ mergePolicy: Replace
+ engineVersion: v2
+ data:
+ secrets.json: |
+ {
+ "username": "{{ .USERNAME }}",
+ "password": "{{ .PASSWORD }}",
+ "app_apikey": "{{ .APIKEY }}",
+ "app_properties": "{{ .APP_PROPERTIES }}"
+ }
+
+{% endraw %}
diff --git a/go.mod b/go.mod
index 3dc60a14d71..0c85855da8a 100644
--- a/go.mod
+++ b/go.mod
@@ -151,6 +151,7 @@ require (
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
+ github.com/go-chef/chef v0.28.4
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/errors v0.21.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
diff --git a/go.sum b/go.sum
index 4f70c00979d..8046b488ba8 100644
--- a/go.sum
+++ b/go.sum
@@ -200,6 +200,8 @@ github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbi
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/ctdk/goiardi v0.11.10 h1:IB/3Afl1pC2Q4KGwzmhHPAoJfe8VtU51wZ2V0QkvsL0=
+github.com/ctdk/goiardi v0.11.10/go.mod h1:Pr6Cj6Wsahw45myttaOEZeZ0LE7p1qzWmzgsBISkrNI=
github.com/cyberark/conjur-api-go v0.11.1 h1:vjaMkw0geJsA+ikMM6UDLg4VLFQWKo/B0i9IWlOQ1f0=
github.com/cyberark/conjur-api-go v0.11.1/go.mod h1:n1p46Hj9l8wkZjM17cVYdfcatyPboWyioLGlC0QszCs=
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
@@ -245,6 +247,8 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-chef/chef v0.28.4 h1:NvvEfBnS9sv6y+9NiBKf01kVAK+4LDKnCpYV8LjMi90=
+github.com/go-chef/chef v0.28.4/go.mod h1:7RU1oCrRErTrkmIszkhJ9vHw7Bv2hZ1Vv1C1qKj01fc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -583,6 +587,8 @@ github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqSc
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/r3labs/diff v0.0.0-20191120142937-b4ed99a31f5a h1:2v4Ipjxa3sh+xn6GvtgrMub2ci4ZLQMvTaYIba2lfdc=
+github.com/r3labs/diff v0.0.0-20191120142937-b4ed99a31f5a/go.mod h1:ozniNEFS3j1qCwHKdvraMn1WJOsUxHd7lYfukEIS4cs=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml
index e52a078506b..9b737dfac7a 100644
--- a/hack/api-docs/mkdocs.yml
+++ b/hack/api-docs/mkdocs.yml
@@ -89,6 +89,7 @@ nav:
- AWS Secrets Manager: provider/aws-secrets-manager.md
- AWS Parameter Store: provider/aws-parameter-store.md
- Azure Key Vault: provider/azure-key-vault.md
+ - Chef: provider/chef.md
- CyberArk Conjur: provider/conjur.md
- Google Cloud Secret Manager: provider/google-secrets-manager.md
- HashiCorp Vault: provider/hashicorp-vault.md
diff --git a/pkg/provider/chef/chef.go b/pkg/provider/chef/chef.go
new file mode 100644
index 00000000000..ab118e0e61e
--- /dev/null
+++ b/pkg/provider/chef/chef.go
@@ -0,0 +1,343 @@
+/*
+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 chef
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/go-chef/chef"
+ "github.com/go-logr/logr"
+ "github.com/tidwall/gjson"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ kclient "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ "github.com/external-secrets/external-secrets/pkg/metrics"
+ "github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+ errChefStore = "received invalid Chef SecretStore resource: %w"
+ errMissingStore = "missing store"
+ errMissingStoreSpec = "missing store spec"
+ errMissingProvider = "missing provider"
+ errMissingChefProvider = "missing chef provider"
+ errMissingUserName = "missing username"
+ errMissingServerURL = "missing serverurl"
+ errMissingAuth = "cannot initialize Chef Client: no valid authType was specified"
+ errMissingSecretKey = "missing Secret Key"
+ errInvalidClusterStoreMissingPKNamespace = "invalid ClusterSecretStore: missing privateKeySecretRef.Namespace"
+ errFetchK8sSecret = "could not fetch SecretKey Secret: %w"
+ errInvalidURL = "invalid serverurl: %w"
+ errChefClient = "unable to create chef client: %w"
+ errChefProvider = "missing or invalid spec: %w"
+ errUninitalizedChefProvider = "chef provider is not initialized"
+ errNoDatabagItemFound = "data bag item %s not found in data bag %s"
+ errNoDatabagItemPropertyFound = "property %s not found in data bag item"
+ errCannotListDataBagItems = "unable to list items in data bag %s, may be given data bag doesn't exists or it is empty"
+ errUnableToConvertToJSON = "unable to convert databagItem into JSON"
+ errInvalidFormat = "invalid key format in data section. Expected value 'databagName/databagItemName'"
+ errStoreValidateFailed = "unable to validate provided store. Check if username, serverUrl and privateKey are correct"
+ errServerURLNoEndSlash = "serverurl does not end with slash(/)"
+ errInvalidDataform = "invalid key format in dataForm section. Expected only 'databagName'"
+
+ ProviderChef = "Chef"
+ CallChefGetDataBagItem = "GetDataBagItem"
+ CallChefListDataBagItems = "ListDataBagItems"
+ CallChefGetUser = "GetUser"
+)
+
+var contextTimeout = time.Second * 25
+
+type DatabagFetcher interface {
+ GetItem(databagName string, databagItem string) (item chef.DataBagItem, err error)
+ ListItems(name string) (data *chef.DataBagListResult, err error)
+}
+
+type UserInterface interface {
+ Get(name string) (user chef.User, err error)
+}
+
+type Providerchef struct {
+ clientName string
+ databagService DatabagFetcher
+ userService UserInterface
+ log logr.Logger
+}
+
+var _ v1beta1.SecretsClient = &Providerchef{}
+var _ v1beta1.Provider = &Providerchef{}
+
+func init() {
+ v1beta1.Register(&Providerchef{}, &v1beta1.SecretStoreProvider{
+ Chef: &v1beta1.ChefProvider{},
+ })
+}
+
+func (providerchef *Providerchef) NewClient(ctx context.Context, store v1beta1.GenericStore, kube kclient.Client, namespace string) (v1beta1.SecretsClient, error) {
+ chefProvider, err := getChefProvider(store)
+ if err != nil {
+ return nil, fmt.Errorf(errChefProvider, err)
+ }
+
+ credentialsSecret := &corev1.Secret{}
+ objectKey := types.NamespacedName{
+ Name: chefProvider.Auth.SecretRef.SecretKey.Name,
+ Namespace: namespace,
+ }
+
+ if store.GetObjectKind().GroupVersionKind().Kind == v1beta1.ClusterSecretStoreKind {
+ if chefProvider.Auth.SecretRef.SecretKey.Namespace == nil {
+ return nil, fmt.Errorf(errInvalidClusterStoreMissingPKNamespace)
+ }
+ objectKey.Namespace = *chefProvider.Auth.SecretRef.SecretKey.Namespace
+ }
+
+ if err := kube.Get(ctx, objectKey, credentialsSecret); err != nil {
+ return nil, fmt.Errorf(errFetchK8sSecret, err)
+ }
+
+ secretKey := credentialsSecret.Data[chefProvider.Auth.SecretRef.SecretKey.Key]
+ if len(secretKey) == 0 {
+ return nil, fmt.Errorf(errMissingSecretKey)
+ }
+
+ client, err := chef.NewClient(&chef.Config{
+ Name: chefProvider.UserName,
+ Key: string(secretKey),
+ BaseURL: chefProvider.ServerURL,
+ })
+ if err != nil {
+ return nil, fmt.Errorf(errChefClient, err)
+ }
+
+ providerchef.clientName = chefProvider.UserName
+ providerchef.databagService = client.DataBags
+ providerchef.userService = client.Users
+ providerchef.log = ctrl.Log.WithName("provider").WithName("chef").WithName("secretsmanager")
+ return providerchef, nil
+}
+
+// Close closes the client connection.
+func (providerchef *Providerchef) Close(_ context.Context) error {
+ return nil
+}
+
+// Validate checks if the client is configured correctly
+// to be able to retrieve secrets from the provider.
+func (providerchef *Providerchef) Validate() (v1beta1.ValidationResult, error) {
+ _, err := providerchef.userService.Get(providerchef.clientName)
+ metrics.ObserveAPICall(ProviderChef, CallChefGetUser, err)
+ if err != nil {
+ return v1beta1.ValidationResultError, fmt.Errorf(errStoreValidateFailed)
+ }
+ return v1beta1.ValidationResultReady, nil
+}
+
+// GetAllSecrets Retrieves a map[string][]byte with the Databag names as key and the Databag's Items as secrets.
+func (providerchef *Providerchef) GetAllSecrets(_ context.Context, _ v1beta1.ExternalSecretFind) (map[string][]byte, error) {
+ return nil, fmt.Errorf("dataFrom.find not suppported")
+}
+
+// GetSecret returns a databagItem present in the databag. format example: databagName/databagItemName.
+func (providerchef *Providerchef) GetSecret(ctx context.Context, ref v1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+ if utils.IsNil(providerchef.databagService) {
+ return nil, fmt.Errorf(errUninitalizedChefProvider)
+ }
+
+ key := ref.Key
+ databagName := ""
+ databagItem := ""
+ nameSplitted := strings.Split(key, "/")
+ if len(nameSplitted) > 1 {
+ databagName = nameSplitted[0]
+ databagItem = nameSplitted[1]
+ }
+ providerchef.log.Info("fetching secret value", "databag Name:", databagName, "databag Item:", databagItem)
+ if databagName != "" && databagItem != "" {
+ return getSingleDatabagItemWithContext(ctx, providerchef, databagName, databagItem, ref.Property)
+ }
+
+ return nil, fmt.Errorf(errInvalidFormat)
+}
+
+func getSingleDatabagItemWithContext(ctx context.Context, providerchef *Providerchef, dataBagName, databagItemName, propertyName string) ([]byte, error) {
+ ctxWithTimeout, cancel := context.WithTimeout(ctx, contextTimeout)
+ defer cancel()
+ type result = struct {
+ values []byte
+ err error
+ }
+ getWithTimeout := func() chan result {
+ resultChan := make(chan result, 1)
+ go func() {
+ defer close(resultChan)
+ ditem, err := providerchef.databagService.GetItem(dataBagName, databagItemName)
+ metrics.ObserveAPICall(ProviderChef, CallChefGetDataBagItem, err)
+ if err != nil {
+ resultChan <- result{err: fmt.Errorf(errNoDatabagItemFound, databagItemName, dataBagName)}
+ return
+ }
+ jsonByte, err := json.Marshal(ditem)
+ if err != nil {
+ resultChan <- result{err: fmt.Errorf(errUnableToConvertToJSON)}
+ return
+ }
+ if propertyName != "" {
+ propertyValue, err := getPropertyFromDatabagItem(jsonByte, propertyName)
+ if err != nil {
+ resultChan <- result{err: err}
+ return
+ }
+ resultChan <- result{values: propertyValue}
+ } else {
+ resultChan <- result{values: jsonByte}
+ }
+ }()
+ return resultChan
+ }
+ select {
+ case <-ctxWithTimeout.Done():
+ return nil, ctxWithTimeout.Err()
+ case r := <-getWithTimeout():
+ if r.err != nil {
+ return nil, r.err
+ }
+ return r.values, nil
+ }
+}
+
+/*
+A path is a series of keys separated by a dot.
+A key may contain special wildcard characters '*' and '?'.
+To access an array value use the index as the key.
+To get the number of elements in an array or to access a child path, use the '#' character.
+The dot and wildcard characters can be escaped with '\'.
+
+refer https://github.com/tidwall/gjson#:~:text=JSON%20byte%20slices.-,Path%20Syntax,-Below%20is%20a
+*/
+func getPropertyFromDatabagItem(jsonByte []byte, propertyName string) ([]byte, error) {
+ result := gjson.GetBytes(jsonByte, propertyName)
+
+ if !result.Exists() {
+ return nil, fmt.Errorf(errNoDatabagItemPropertyFound, propertyName)
+ }
+ return []byte(result.Str), nil
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.key
+// dataFrom.extract.key only accepts dataBagName, example : dataFrom.extract.key: myDatabag
+// databagItemName or Property not expected in key.
+func (providerchef *Providerchef) GetSecretMap(ctx context.Context, ref v1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+ if utils.IsNil(providerchef.databagService) {
+ return nil, fmt.Errorf(errUninitalizedChefProvider)
+ }
+ databagName := ref.Key
+
+ if strings.Contains(databagName, "/") {
+ return nil, fmt.Errorf(errInvalidDataform)
+ }
+ getAllSecrets := make(map[string][]byte)
+ providerchef.log.Info("fetching all items from", "databag:", databagName)
+ dataItems, err := providerchef.databagService.ListItems(databagName)
+ metrics.ObserveAPICall(ProviderChef, CallChefListDataBagItems, err)
+ if err != nil {
+ return nil, fmt.Errorf(errCannotListDataBagItems, databagName)
+ }
+
+ for dataItem := range *dataItems {
+ dItem, err := getSingleDatabagItemWithContext(ctx, providerchef, databagName, dataItem, "")
+ if err != nil {
+ return nil, fmt.Errorf(errNoDatabagItemFound, dataItem, databagName)
+ }
+ getAllSecrets[dataItem] = dItem
+ }
+ return getAllSecrets, nil
+}
+
+// ValidateStore checks if the provided store is valid.
+func (providerchef *Providerchef) ValidateStore(store v1beta1.GenericStore) (admission.Warnings, error) {
+ chefProvider, err := getChefProvider(store)
+ if err != nil {
+ return nil, fmt.Errorf(errChefStore, err)
+ }
+ // check namespace compared to kind
+ if err := utils.ValidateSecretSelector(store, chefProvider.Auth.SecretRef.SecretKey); err != nil {
+ return nil, fmt.Errorf(errChefStore, err)
+ }
+ return nil, nil
+}
+
+// getChefProvider validates the incoming store and return the chef provider.
+func getChefProvider(store v1beta1.GenericStore) (*v1beta1.ChefProvider, error) {
+ if store == nil {
+ return nil, fmt.Errorf(errMissingStore)
+ }
+ storeSpec := store.GetSpec()
+ if storeSpec == nil {
+ return nil, fmt.Errorf(errMissingStoreSpec)
+ }
+ provider := storeSpec.Provider
+ if provider == nil {
+ return nil, fmt.Errorf(errMissingProvider)
+ }
+ chefProvider := storeSpec.Provider.Chef
+ if chefProvider == nil {
+ return nil, fmt.Errorf(errMissingChefProvider)
+ }
+ if chefProvider.UserName == "" {
+ return chefProvider, fmt.Errorf(errMissingUserName)
+ }
+ if chefProvider.ServerURL == "" {
+ return chefProvider, fmt.Errorf(errMissingServerURL)
+ }
+ if !strings.HasSuffix(chefProvider.ServerURL, "/") {
+ return chefProvider, fmt.Errorf(errServerURLNoEndSlash)
+ }
+ // check valid URL
+ if _, err := url.ParseRequestURI(chefProvider.ServerURL); err != nil {
+ return chefProvider, fmt.Errorf(errInvalidURL, err)
+ }
+ if chefProvider.Auth == nil {
+ return chefProvider, fmt.Errorf(errMissingAuth)
+ }
+ if chefProvider.Auth.SecretRef.SecretKey.Key == "" {
+ return chefProvider, fmt.Errorf(errMissingSecretKey)
+ }
+
+ return chefProvider, nil
+}
+
+// Not Implemented DeleteSecret.
+func (providerchef *Providerchef) DeleteSecret(_ context.Context, _ v1beta1.PushSecretRemoteRef) error {
+ return fmt.Errorf("not implemented")
+}
+
+// Not Implemented PushSecret.
+func (providerchef *Providerchef) PushSecret(_ context.Context, _ *corev1.Secret, _ v1beta1.PushSecretData) error {
+ return fmt.Errorf("not implemented")
+}
+
+// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
+func (providerchef *Providerchef) Capabilities() v1beta1.SecretStoreCapabilities {
+ return v1beta1.SecretStoreReadOnly
+}
diff --git a/pkg/provider/chef/chef_test.go b/pkg/provider/chef/chef_test.go
new file mode 100644
index 00000000000..107e2471d98
--- /dev/null
+++ b/pkg/provider/chef/chef_test.go
@@ -0,0 +1,428 @@
+/*
+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 chef
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/go-chef/chef"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+ fake "github.com/external-secrets/external-secrets/pkg/provider/chef/fake"
+ "github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+ name = "chef-demo-user"
+ baseURL = "https://chef.cloudant.com/organizations/myorg/"
+ noEndSlashInvalidBaseURL = "no end slash invalid base URL"
+ baseInvalidURL = "invalid base URL/"
+ authName = "chef-demo-auth-name"
+ authKey = "chef-demo-auth-key"
+ authNamespace = "chef-demo-auth-namespace"
+ kind = "SecretStore"
+ apiversion = "external-secrets.io/v1beta1"
+ databagName = "databag01"
+)
+
+type chefTestCase struct {
+ mockClient *fake.ChefMockClient
+ databagName string
+ databagItemName string
+ property string
+ ref *esv1beta1.ExternalSecretDataRemoteRef
+ apiErr error
+ expectError string
+ expectedData map[string][]byte
+ expectedByte []byte
+}
+
+type ValidateStoreTestCase struct {
+ store *esv1beta1.SecretStore
+ err error
+}
+
+// type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore
+
+func makeValidChefTestCase() *chefTestCase {
+ smtc := chefTestCase{
+ mockClient: &fake.ChefMockClient{},
+ databagName: "databag01",
+ databagItemName: "item01",
+ property: "",
+ apiErr: nil,
+ expectError: "",
+ expectedData: map[string][]byte{"item01": []byte(`"https://chef.com/organizations/dev/data/databag01/item01"`)},
+ expectedByte: []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`),
+ }
+
+ smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property)
+ smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr)
+ smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr)
+ return &smtc
+}
+
+func makeInValidChefTestCase() *chefTestCase {
+ smtc := chefTestCase{
+ mockClient: &fake.ChefMockClient{},
+ databagName: "databag01",
+ databagItemName: "item03",
+ property: "",
+ apiErr: errors.New("unable to convert databagItem into JSON"),
+ expectError: "unable to convert databagItem into JSON",
+ expectedData: nil,
+ expectedByte: nil,
+ }
+
+ smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property)
+ smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr)
+ smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr)
+ return &smtc
+}
+
+func makeValidRef(databag, dataitem, property string) *esv1beta1.ExternalSecretDataRemoteRef {
+ return &esv1beta1.ExternalSecretDataRemoteRef{
+ Key: databag + "/" + dataitem,
+ Property: property,
+ }
+}
+
+func makeinValidRef() *esv1beta1.ExternalSecretDataRemoteRef {
+ return &esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "",
+ }
+}
+
+func makeValidRefForGetSecretMap(databag string) *esv1beta1.ExternalSecretDataRemoteRef {
+ return &esv1beta1.ExternalSecretDataRemoteRef{
+ Key: databag,
+ }
+}
+
+func makeValidChefTestCaseCustom(tweaks ...func(smtc *chefTestCase)) *chefTestCase {
+ smtc := makeValidChefTestCase()
+ for _, fn := range tweaks {
+ fn(smtc)
+ }
+ return smtc
+}
+
+func TestChefGetSecret(t *testing.T) {
+ nilClient := func(smtc *chefTestCase) {
+ smtc.mockClient = nil
+ smtc.expectedByte = nil
+ smtc.expectError = "chef provider is not initialized"
+ }
+
+ invalidDatabagName := func(smtc *chefTestCase) {
+ smtc.databagName = "databag02"
+ smtc.expectedByte = nil
+ smtc.ref = makeinValidRef()
+ smtc.expectError = "invalid key format in data section. Expected value 'databagName/databagItemName'"
+ }
+
+ invalidDatabagItemName := func(smtc *chefTestCase) {
+ smtc.expectError = "data bag item item02 not found in data bag databag01"
+ smtc.databagName = databagName
+ smtc.databagItemName = "item02"
+ smtc.expectedByte = nil
+ smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "")
+ }
+
+ noProperty := func(smtc *chefTestCase) {
+ smtc.expectError = "property findProperty not found in data bag item"
+ smtc.databagName = databagName
+ smtc.databagItemName = "item01"
+ smtc.expectedByte = nil
+ smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty")
+ }
+
+ withProperty := func(smtc *chefTestCase) {
+ smtc.expectedByte = []byte("foundProperty")
+ smtc.databagName = "databag03"
+ smtc.databagItemName = "item03"
+ smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty")
+ }
+
+ successCases := []*chefTestCase{
+ makeValidChefTestCase(),
+ makeValidChefTestCaseCustom(nilClient),
+ makeValidChefTestCaseCustom(invalidDatabagName),
+ makeValidChefTestCaseCustom(invalidDatabagItemName),
+ makeValidChefTestCaseCustom(noProperty),
+ makeValidChefTestCaseCustom(withProperty),
+ makeInValidChefTestCase(),
+ }
+
+ sm := Providerchef{
+ databagService: &chef.DataBagService{},
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ for k, v := range successCases {
+ sm.databagService = v.mockClient
+ out, err := sm.GetSecret(ctx, *v.ref)
+ if err != nil && !utils.ErrorContains(err, v.expectError) {
+ t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err)
+ } else if v.expectError != "" && err == nil {
+ t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError)
+ }
+ if !bytes.Equal(out, v.expectedByte) {
+ t.Errorf("[case %d] expected secret: %s, got: %s", k, v.expectedByte, out)
+ }
+ }
+}
+
+func TestChefGetSecretMap(t *testing.T) {
+ nilClient := func(smtc *chefTestCase) {
+ smtc.mockClient = nil
+ smtc.expectedByte = nil
+ smtc.expectError = "chef provider is not initialized"
+ }
+
+ databagHasSlash := func(smtc *chefTestCase) {
+ smtc.expectedByte = nil
+ smtc.ref = makeinValidRef()
+ smtc.ref.Key = "data/Bag02"
+ smtc.expectError = "invalid key format in dataForm section. Expected only 'databagName'"
+ }
+
+ withProperty := func(smtc *chefTestCase) {
+ smtc.expectedByte = []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`)
+ smtc.databagName = databagName
+ smtc.ref = makeValidRefForGetSecretMap(smtc.databagName)
+ }
+
+ withProperty2 := func(smtc *chefTestCase) {
+ smtc.expectError = "unable to list items in data bag 123, may be given data bag doesn't exists or it is empty"
+ smtc.expectedByte = nil
+ smtc.databagName = "123"
+ smtc.ref = makeValidRefForGetSecretMap(smtc.databagName)
+ }
+
+ successCases := []*chefTestCase{
+ makeValidChefTestCaseCustom(nilClient),
+ makeValidChefTestCaseCustom(databagHasSlash),
+ makeValidChefTestCaseCustom(withProperty),
+ makeValidChefTestCaseCustom(withProperty2),
+ }
+
+ pc := Providerchef{
+ databagService: &chef.DataBagService{},
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ for k, v := range successCases {
+ pc.databagService = v.mockClient
+ out, err := pc.GetSecretMap(ctx, *v.ref)
+ if err != nil && !utils.ErrorContains(err, v.expectError) {
+ t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err)
+ } else if v.expectError != "" && err == nil {
+ t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError)
+ }
+ if !bytes.Equal(out["item01"], v.expectedByte) {
+ t.Errorf("[case %d] unexpected secret: expected %s, got %s", k, v.expectedByte, out)
+ }
+ }
+}
+
+func makeSecretStore(name, baseURL string, auth *esv1beta1.ChefAuth) *esv1beta1.SecretStore {
+ store := &esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Chef: &esv1beta1.ChefProvider{
+ UserName: name,
+ ServerURL: baseURL,
+ Auth: auth,
+ },
+ },
+ },
+ }
+ return store
+}
+
+func makeAuth(name, namespace, key string) *esv1beta1.ChefAuth {
+ return &esv1beta1.ChefAuth{
+ SecretRef: esv1beta1.ChefAuthSecretRef{
+ SecretKey: v1.SecretKeySelector{
+ Name: name,
+ Key: key,
+ Namespace: &namespace,
+ },
+ },
+ }
+}
+
+func TestValidateStore(t *testing.T) {
+ testCases := []ValidateStoreTestCase{
+ {
+ store: makeSecretStore("", baseURL, makeAuth(authName, authNamespace, authKey)),
+ err: fmt.Errorf("received invalid Chef SecretStore resource: missing username"),
+ },
+ {
+ store: makeSecretStore(name, "", makeAuth(authName, authNamespace, authKey)),
+ err: fmt.Errorf("received invalid Chef SecretStore resource: missing serverurl"),
+ },
+ {
+ store: makeSecretStore(name, baseURL, nil),
+ err: fmt.Errorf("received invalid Chef SecretStore resource: cannot initialize Chef Client: no valid authType was specified"),
+ },
+ {
+ store: makeSecretStore(name, baseInvalidURL, makeAuth(authName, authNamespace, authKey)),
+ err: fmt.Errorf("received invalid Chef SecretStore resource: invalid serverurl: parse \"invalid base URL/\": invalid URI for request"),
+ },
+ {
+ store: makeSecretStore(name, noEndSlashInvalidBaseURL, makeAuth(authName, authNamespace, authKey)),
+ err: fmt.Errorf("received invalid Chef SecretStore resource: serverurl does not end with slash(/)"),
+ },
+ {
+ store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, "")),
+ err: fmt.Errorf("received invalid Chef SecretStore resource: missing Secret Key"),
+ },
+ {
+ store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, authKey)),
+ err: fmt.Errorf("received invalid Chef SecretStore resource: namespace not allowed with namespaced SecretStore"),
+ },
+ {
+ store: &esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: nil,
+ },
+ },
+ err: fmt.Errorf("received invalid Chef SecretStore resource: missing provider"),
+ },
+ {
+ store: &esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Chef: nil,
+ },
+ },
+ },
+ err: fmt.Errorf("received invalid Chef SecretStore resource: missing chef provider"),
+ },
+ }
+ pc := Providerchef{}
+ for _, tc := range testCases {
+ _, err := pc.ValidateStore(tc.store)
+ if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
+ t.Errorf("test failed! want: %v, got: %v", tc.err, err)
+ } else if tc.err == nil && err != nil {
+ t.Errorf("want: nil got: err %v", err)
+ } else if tc.err != nil && err == nil {
+ t.Errorf("want: err %v got: nil", tc.err)
+ }
+ }
+}
+
+func TestNewClient(t *testing.T) {
+ store := &esv1beta1.SecretStore{TypeMeta: metav1.TypeMeta{Kind: "ClusterSecretStore"},
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Chef: &esv1beta1.ChefProvider{
+ Auth: makeAuth(authName, authNamespace, authKey),
+ UserName: name,
+ ServerURL: baseURL,
+ },
+ },
+ },
+ }
+
+ expected := fmt.Sprintf("could not fetch SecretKey Secret: secrets %q not found", authName)
+ expectedMissingStore := "missing or invalid spec: missing store"
+
+ ctx := context.TODO()
+
+ kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "creds",
+ Namespace: "default",
+ }, TypeMeta: metav1.TypeMeta{
+ Kind: kind,
+ APIVersion: apiversion,
+ },
+ }).Build()
+
+ pc := Providerchef{databagService: &fake.ChefMockClient{}}
+ _, errMissingStore := pc.NewClient(ctx, nil, kube, "default")
+ if !ErrorContains(errMissingStore, expectedMissingStore) {
+ t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", errMissingStore.Error(), expectedMissingStore)
+ }
+ _, err := pc.NewClient(ctx, store, kube, "default")
+ if !ErrorContains(err, expected) {
+ t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", err.Error(), expected)
+ }
+}
+
+func ErrorContains(out error, want string) bool {
+ if out == nil {
+ return want == ""
+ }
+ if want == "" {
+ return false
+ }
+ return strings.Contains(out.Error(), want)
+}
+
+func TestValidate(t *testing.T) {
+ pc := Providerchef{}
+ var mockClient *fake.ChefMockClient
+ pc.userService = mockClient
+ pc.clientName = "correctUser"
+ _, err := pc.Validate()
+ t.Log("Error: ", err)
+ pc.clientName = "wrongUser"
+ _, err = pc.Validate()
+ t.Log("Error: ", err)
+}
+
+func TestCapabilities(t *testing.T) {
+ pc := Providerchef{}
+ capabilities := pc.Capabilities()
+ t.Log(capabilities)
+}
+
+// Test Cases To be added when Close function is implemented.
+func TestClose(_ *testing.T) {
+ pc := Providerchef{}
+ pc.Close(context.Background())
+}
+
+// Test Cases To be added when GetAllSecrets function is implemented.
+func TestGetAllSecrets(_ *testing.T) {
+ pc := Providerchef{}
+ pc.GetAllSecrets(context.Background(), esv1beta1.ExternalSecretFind{})
+}
+
+// Test Cases To be implemented when DeleteSecret function is implemented.
+func TestDeleteSecret(_ *testing.T) {
+ pc := Providerchef{}
+ pc.DeleteSecret(context.Background(), nil)
+}
+
+// Test Cases To be implemented when PushSecret function is implemented.
+func TestPushSecret(_ *testing.T) {
+ pc := Providerchef{}
+ pc.PushSecret(context.Background(), &corev1.Secret{}, nil)
+}
diff --git a/pkg/provider/chef/fake/fake.go b/pkg/provider/chef/fake/fake.go
new file mode 100644
index 00000000000..41466fe1e90
--- /dev/null
+++ b/pkg/provider/chef/fake/fake.go
@@ -0,0 +1,104 @@
+/*
+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 fake
+
+import (
+ "errors"
+ "fmt"
+ "math"
+
+ "github.com/go-chef/chef"
+)
+
+const (
+ CORRECTUSER = "correctUser"
+ testitem = "item03"
+ DatabagName = "databag01"
+)
+
+type ChefMockClient struct {
+ getItem func(databagName string, databagItem string) (item chef.DataBagItem, err error)
+ listItems func(name string) (data *chef.DataBagListResult, err error)
+ getUser func(name string) (user chef.User, err error)
+}
+
+func (mc *ChefMockClient) GetItem(databagName, databagItem string) (item chef.DataBagItem, err error) {
+ return mc.getItem(databagName, databagItem)
+}
+
+func (mc *ChefMockClient) ListItems(name string) (data *chef.DataBagListResult, err error) {
+ return mc.listItems(name)
+}
+
+func (mc *ChefMockClient) Get(name string) (user chef.User, err error) {
+ if name == CORRECTUSER {
+ user = chef.User{
+ UserName: name,
+ }
+ err = nil
+ } else {
+ user = chef.User{}
+ err = errors.New("message")
+ }
+ return user, err
+}
+
+func (mc *ChefMockClient) WithItem(_, _ string, _ error) {
+ if mc != nil {
+ mc.getItem = func(dataBagName, databagItemName string) (item chef.DataBagItem, err error) {
+ ret := make(map[string]interface{})
+ switch {
+ case dataBagName == DatabagName && databagItemName == "item01":
+ jsonstring := `{"id":"` + dataBagName + `-` + databagItemName + `","some_key":"fe7f29ede349519a1","some_password":"dolphin_123zc","some_username":"testuser"}`
+ ret[databagItemName] = jsonstring
+ case dataBagName == "databag03" && databagItemName == testitem:
+ jsonMap := make(map[string]string)
+ jsonMap["id"] = testitem
+ jsonMap["findProperty"] = "foundProperty"
+ return jsonMap, nil
+ case dataBagName == DatabagName && databagItemName == testitem:
+ return math.Inf(1), nil
+ default:
+ str := "https://chef.com/organizations/dev/data/" + dataBagName + "/" + databagItemName + ": 404"
+ return nil, errors.New(str)
+ }
+ return ret, nil
+ }
+ }
+}
+
+func (mc *ChefMockClient) WithListItems(_ string, _ error) {
+ if mc != nil {
+ mc.listItems = func(databagName string) (data *chef.DataBagListResult, err error) {
+ ret := make(chef.DataBagListResult)
+ if databagName == DatabagName {
+ jsonstring := fmt.Sprintf("https://chef.com/organizations/dev/data/%s/item01", databagName)
+ ret["item01"] = jsonstring
+ } else {
+ return nil, fmt.Errorf("data bag not found: %s", databagName)
+ }
+ return &ret, nil
+ }
+ }
+}
+
+func (mc *ChefMockClient) WithUser(_ string, _ error) {
+ if mc != nil {
+ mc.getUser = func(name string) (user chef.User, err error) {
+ return chef.User{
+ UserName: name,
+ }, nil
+ }
+ }
+}
diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go
index 68004c4a041..c43b8012f0f 100644
--- a/pkg/provider/register/register.go
+++ b/pkg/provider/register/register.go
@@ -22,6 +22,7 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
+ _ "github.com/external-secrets/external-secrets/pkg/provider/chef"
_ "github.com/external-secrets/external-secrets/pkg/provider/conjur"
_ "github.com/external-secrets/external-secrets/pkg/provider/delinea"
_ "github.com/external-secrets/external-secrets/pkg/provider/doppler"