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

Implementation of Chef External Secrets Provider (#3127)

* Adding the details for chef provider secret store.

Issue: https://github.com/external-secrets/external-secrets/issues/2905

This commit intends to add the chef provider structure to the existing list of external-secrets providers.
It defines the structure of the SecretStore and ClusterSecretStore for chef Provider.
The yaml resource will contain 3 important parts to identify and connect to chef server to reconcile secrets. They are:
1. serverurl: This is the URL to the chef server.
2. username: The username to connect to the chef server.
3. auth: The password to connect to the chef server. It is a reference to an already existing kubernetes secret containing the password.

This commit also contains the auto generated CRDs using the `make generate` command.

Signed-off-by: Subroto Roy <subrotoroy007@gmail.com>

* Implementation for Chef ESO provided

Signed-off-by: vardhanreddy13 <vvv.vardhanreddy@gmail.com>

* - implemented Chef eso, added required methods
- added unit test cases
- added sample documentation
Issue: https://github.com/external-secrets/external-secrets/issues/2905

Signed-off-by: Sourav Patnaik <souravpatnaik123@gmail.com>

* Added Documentation for Authentication

Signed-off-by: Subroto Roy <subrotoroy007@gmail.com>

* added documentation for Chef eso
Issue: https://github.com/external-secrets/external-secrets/issues/2905

Signed-off-by: Sourav Patnaik <souravpatnaik123@gmail.com>

* Updated chef ESO documentation

Signed-off-by: vardhanreddy13 <vvv.vardhanreddy@gmail.com>

* updated ValidateStore method signature
Issue: https://github.com/external-secrets/external-secrets/issues/2905

Signed-off-by: Sourav Patnaik <souravpatnaik123@gmail.com>

* made changes in chef provider to satisfy 'make docs'

Issue: https://github.com/external-secrets/external-secrets/issues/2905

Signed-off-by: Sourav Patnaik <souravpatnaik123@gmail.com>

* - updated code as per review comment, make reviewable suggestions
Issue: https://github.com/external-secrets/external-secrets/issues/2905

Signed-off-by: Sourav Patnaik <souravpatnaik123@gmail.com>

* modified chef provider code as per review comment

Issue: https://github.com/external-secrets/external-secrets/issues/2905

Signed-off-by: Sourav Patnaik <souravpatnaik123@gmail.com>

---------

Signed-off-by: Subroto Roy <subrotoroy007@gmail.com>
Signed-off-by: vardhanreddy13 <vvv.vardhanreddy@gmail.com>
Signed-off-by: Sourav Patnaik <souravpatnaik123@gmail.com>
Co-authored-by: Subroto Roy <subrotoroy007@gmail.com>
Co-authored-by: vardhanreddy13 <vvv.vardhanreddy@gmail.com>
This commit is contained in:
Sourav Patnaik 2024-02-14 14:24:08 +05:30 committed by GitHub
parent e726087851
commit a012f4829c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1461 additions and 0 deletions

View file

@ -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"`
}

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -1108,6 +1108,123 @@ External Secrets meta/v1.SecretKeySelector
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.ChefAuth">ChefAuth
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.ChefProvider">ChefProvider</a>)
</p>
<p>
<p>ChefAuth contains a secretRef for credentials.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>secretRef</code></br>
<em>
<a href="#external-secrets.io/v1beta1.ChefAuthSecretRef">
ChefAuthSecretRef
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.ChefAuthSecretRef">ChefAuthSecretRef
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.ChefAuth">ChefAuth</a>)
</p>
<p>
<p>ChefAuthSecretRef holds secret references for chef server login credentials.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>privateKeySecretRef</code></br>
<em>
<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
External Secrets meta/v1.SecretKeySelector
</a>
</em>
</td>
<td>
<p>SecretKey is the Signing Key in PEM format, used for authentication.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.ChefProvider">ChefProvider
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
</p>
<p>
<p>ChefProvider configures a store to sync secrets using basic chef server connection credentials.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>auth</code></br>
<em>
<a href="#external-secrets.io/v1beta1.ChefAuth">
ChefAuth
</a>
</em>
</td>
<td>
<p>Auth defines the information necessary to authenticate against chef Server</p>
</td>
</tr>
<tr>
<td>
<code>username</code></br>
<em>
string
</em>
</td>
<td>
<p>UserName should be the user ID on the chef server</p>
</td>
</tr>
<tr>
<td>
<code>serverUrl</code></br>
<em>
string
</em>
</td>
<td>
<p>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 &ldquo;/&rdquo;</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.ClusterExternalSecret">ClusterExternalSecret
</h3>
<p>
@ -5289,6 +5406,20 @@ DelineaProvider
<a href="https://docs.delinea.com/online-help/products/devops-secrets-vault/current">https://docs.delinea.com/online-help/products/devops-secrets-vault/current</a></p>
</td>
</tr>
<tr>
<td>
<code>chef</code></br>
<em>
<a href="#external-secrets.io/v1beta1.ChefProvider">
ChefProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Chef configures this store to sync secrets with chef server</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef

113
docs/provider/chef.md Normal file
View file

@ -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

View file

@ -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 %}

1
go.mod
View file

@ -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

6
go.sum
View file

@ -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=

View file

@ -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

343
pkg/provider/chef/chef.go Normal file
View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}
}
}

View file

@ -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"