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

Add device42 provider (#3571)

This commit is contained in:
smcavallo 2024-06-14 00:04:19 -04:00 committed by GitHub
parent e01fc82ac2
commit d29c001d37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 916 additions and 4 deletions

View file

@ -257,22 +257,22 @@ docker.promote: ## Promote the docker image to the registry
# ====================================================================================
# Terraform
tf.plan.%: ## Runs terrform plan for a provider
tf.plan.%: ## Runs terraform plan for a provider
@cd $(TF_DIR)/$*; \
terraform init; \
terraform plan
tf.apply.%: ## Runs terrform apply for a provider
tf.apply.%: ## Runs terraform apply for a provider
@cd $(TF_DIR)/$*; \
terraform init; \
terraform apply -auto-approve
tf.destroy.%: ## Runs terrform destroy for a provider
tf.destroy.%: ## Runs terraform destroy for a provider
@cd $(TF_DIR)/$*; \
terraform init; \
terraform destroy -auto-approve
tf.show.%: ## Runs terrform show for a provider and outputs to a file
tf.show.%: ## Runs terraform show for a provider and outputs to a file
@cd $(TF_DIR)/$*; \
terraform init; \
terraform plan -out tfplan.binary; \

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"
)
// Device42Provider configures a store to sync secrets with a Device42 instance.
type Device42Provider struct {
// URL configures the Device42 instance URL.
Host string `json:"host"`
// Auth configures how secret-manager authenticates with a Device42 instance.
Auth Device42Auth `json:"auth"`
}
type Device42Auth struct {
SecretRef Device42SecretRef `json:"secretRef"`
}
type Device42SecretRef struct {
// Username / Password is used for authentication.
// +optional
Credentials esmeta.SecretKeySelector `json:"credentials,omitempty"`
}

View file

@ -164,6 +164,10 @@ type SecretStoreProvider struct {
// +optional
Passbolt *PassboltProvider `json:"passbolt,omitempty"`
// Device42 configures this store to sync secrets using the Device42 provider
// +optional
Device42 *Device42Provider `json:"device42,omitempty"`
// Infisical configures this store to sync secrets using the Infisical provider
// +optional
Infisical *InfisicalProvider `json:"infisical,omitempty"`

View file

@ -867,6 +867,54 @@ func (in *DelineaProviderSecretRef) DeepCopy() *DelineaProviderSecretRef {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Device42Auth) DeepCopyInto(out *Device42Auth) {
*out = *in
in.SecretRef.DeepCopyInto(&out.SecretRef)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Device42Auth.
func (in *Device42Auth) DeepCopy() *Device42Auth {
if in == nil {
return nil
}
out := new(Device42Auth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Device42Provider) DeepCopyInto(out *Device42Provider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Device42Provider.
func (in *Device42Provider) DeepCopy() *Device42Provider {
if in == nil {
return nil
}
out := new(Device42Provider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Device42SecretRef) DeepCopyInto(out *Device42SecretRef) {
*out = *in
in.Credentials.DeepCopyInto(&out.Credentials)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Device42SecretRef.
func (in *Device42SecretRef) DeepCopy() *Device42SecretRef {
if in == nil {
return nil
}
out := new(Device42SecretRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DopplerAuth) DeepCopyInto(out *DopplerAuth) {
*out = *in
@ -2357,6 +2405,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(PassboltProvider)
(*in).DeepCopyInto(*out)
}
if in.Device42 != nil {
in, out := &in.Device42, &out.Device42
*out = new(Device42Provider)
(*in).DeepCopyInto(*out)
}
if in.Infisical != nil {
in, out := &in.Infisical, &out.Infisical
*out = new(InfisicalProvider)

View file

@ -2569,6 +2569,45 @@ spec:
- clientSecret
- tenant
type: object
device42:
description: Device42 configures this store to sync secrets using
the Device42 provider
properties:
auth:
description: Auth configures how secret-manager authenticates
with a Device42 instance.
properties:
secretRef:
properties:
credentials:
description: Username / Password is 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
type: object
required:
- secretRef
type: object
host:
description: URL configures the Device42 instance URL.
type: string
required:
- auth
- host
type: object
doppler:
description: Doppler configures this store to sync secrets using
the Doppler provider

View file

@ -2569,6 +2569,45 @@ spec:
- clientSecret
- tenant
type: object
device42:
description: Device42 configures this store to sync secrets using
the Device42 provider
properties:
auth:
description: Auth configures how secret-manager authenticates
with a Device42 instance.
properties:
secretRef:
properties:
credentials:
description: Username / Password is 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
type: object
required:
- secretRef
type: object
host:
description: URL configures the Device42 instance URL.
type: string
required:
- auth
- host
type: object
doppler:
description: Doppler configures this store to sync secrets using
the Doppler provider

View file

@ -3056,6 +3056,42 @@ spec:
- clientSecret
- tenant
type: object
device42:
description: Device42 configures this store to sync secrets using the Device42 provider
properties:
auth:
description: Auth configures how secret-manager authenticates with a Device42 instance.
properties:
secretRef:
properties:
credentials:
description: Username / Password is 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
type: object
required:
- secretRef
type: object
host:
description: URL configures the Device42 instance URL.
type: string
required:
- auth
- host
type: object
doppler:
description: Doppler configures this store to sync secrets using the Doppler provider
properties:
@ -8502,6 +8538,42 @@ spec:
- clientSecret
- tenant
type: object
device42:
description: Device42 configures this store to sync secrets using the Device42 provider
properties:
auth:
description: Auth configures how secret-manager authenticates with a Device42 instance.
properties:
secretRef:
properties:
credentials:
description: Username / Password is 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
type: object
required:
- secretRef
type: object
host:
description: URL configures the Device42 instance URL.
type: string
required:
- auth
- host
type: object
doppler:
description: Doppler configures this store to sync secrets using the Doppler provider
properties:

View file

@ -2234,6 +2234,111 @@ External Secrets meta/v1.SecretKeySelector
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.Device42Auth">Device42Auth
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.Device42Provider">Device42Provider</a>)
</p>
<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.Device42SecretRef">
Device42SecretRef
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.Device42Provider">Device42Provider
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
</p>
<p>
<p>Device42Provider configures a store to sync secrets with a Device42 instance.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>host</code></br>
<em>
string
</em>
</td>
<td>
<p>URL configures the Device42 instance URL.</p>
</td>
</tr>
<tr>
<td>
<code>auth</code></br>
<em>
<a href="#external-secrets.io/v1beta1.Device42Auth">
Device42Auth
</a>
</em>
</td>
<td>
<p>Auth configures how secret-manager authenticates with a Device42 instance.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.Device42SecretRef">Device42SecretRef
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.Device42Auth">Device42Auth</a>)
</p>
<p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>credentials</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>
<em>(Optional)</em>
<p>Username / Password is used for authentication.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.DopplerAuth">DopplerAuth
</h3>
<p>
@ -6192,6 +6297,20 @@ PassboltProvider
</tr>
<tr>
<td>
<code>device42</code></br>
<em>
<a href="#external-secrets.io/v1beta1.Device42Provider">
Device42Provider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Device42 configures this store to sync secrets using the Device42 provider</p>
</td>
</tr>
<tr>
<td>
<code>infisical</code></br>
<em>
<a href="#external-secrets.io/v1beta1.InfisicalProvider">

View file

@ -56,6 +56,7 @@ The following table describes the stability level of each provider and who's res
| [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi) | alpha | [@dirien](https://github.com/dirien) |
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
| [Infisical](https://external-secrets.io/latest/provider/infisical) | alpha | [@akhilmhdh](https://github.com/akhilmhdh) |
| [Device42](https://external-secrets.io/latest/provider/device42) | alpha | |
## Provider Feature Support
@ -86,6 +87,7 @@ The following table show the support for features across different providers.
| Pulumi ESC | x | | | | x | | |
| Passbolt | x | | | | x | | |
| Infisical | x | | | x | x | | |
| Device42 | | | | | x | | |
## Support Policy

58
docs/provider/device42.md Normal file
View file

@ -0,0 +1,58 @@
External Secrets Operator integrates with [Device42 API](https://api.device42.com/#!/Passwords/getPassword) to sync Device42 secrets into a Kubernetes cluster.
### Authentication
`username` and `password` is required to talk to the Device42 API.
```yaml
apiVersion: v1
kind: Secret
metadata:
name: device42-credentials
data:
username: dGVzdA== # "test"
password: dGVzdA== # "test"
```
### Creating a SecretStore
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: device42-secret-store
spec:
provider:
device42:
host: <DEVICE42_HOSTNAME>
auth:
secretRef:
credentials:
name: <NAME_OF_KUBE_SECRET>
key: <KEY_IN_KUBE_SECRET>
namespace: <kube-system>
```
### Referencing Secrets
Secrets can be referenced by defining the `key` containing the Id of the secret.
The `password` field is return from device42
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: device42-external-secret
spec:
refreshInterval: 5m
secretStoreRef:
kind: SecretStore
name: device42-secret-store
target:
name: <K8s_SECRET_NAME_TO_MANAGE>
data:
- secretKey: <KEY_NAME_WITHIN_KUBE_SECRET>
remoteRef:
key: <DEVICE42_SECRET_ID>
```

View file

@ -0,0 +1,16 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: device42-find-by-id
spec:
refreshInterval: 10s
secretStoreRef:
# This name must match the metadata.name in the `SecretStore`
name: device42
kind: SecretStore
target:
name: k8s-secret-to-be-created
data:
- secretKey: K8S_PASSWORD
remoteRef:
key: "12345"

View file

@ -95,6 +95,7 @@ nav:
- Azure Key Vault: provider/azure-key-vault.md
- Chef: provider/chef.md
- CyberArk Conjur: provider/conjur.md
- Device42: provider/device42.md
- Google Cloud Secret Manager: provider/google-secrets-manager.md
- HashiCorp Vault: provider/hashicorp-vault.md
- Kubernetes: provider/kubernetes.md

View file

@ -0,0 +1,182 @@
/*
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 device42
import (
"context"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/utils"
)
const (
errNotImplemented = "not implemented"
errUninitializedProvider = "unable to get device42 client"
errCredSecretName = "credentials are empty"
errInvalidClusterStoreMissingSAKNamespace = "invalid clusterStore missing SAK namespace"
errFetchSAKSecret = "couldn't find secret on cluster: %w"
errMissingSAK = "missing credentials while setting auth"
)
type Client interface {
GetSecret(secretID string) (D42Password, error)
}
// Device42 Provider struct with reference to a Device42 client.
type Device42 struct {
client Client
}
func (p *Device42) ValidateStore(esv1beta1.GenericStore) (admission.Warnings, error) {
return nil, nil
}
func (p *Device42) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// Client for interacting with kubernetes.
type device42Client struct {
kube kclient.Client
store *esv1beta1.Device42Provider
namespace string
storeKind string
}
type Provider struct{}
func (c *device42Client) getAuth(ctx context.Context) (string, string, error) {
credentialsSecret := &corev1.Secret{}
credentialsSecretName := c.store.Auth.SecretRef.Credentials.Name
if credentialsSecretName == "" {
return "", "", fmt.Errorf(errCredSecretName)
}
objectKey := types.NamespacedName{
Name: credentialsSecretName,
Namespace: c.namespace,
}
// only ClusterStore is allowed to set namespace (and then it's required)
if c.storeKind == esv1beta1.ClusterSecretStoreKind {
if c.store.Auth.SecretRef.Credentials.Namespace == nil {
return "", "", fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
}
objectKey.Namespace = *c.store.Auth.SecretRef.Credentials.Namespace
}
err := c.kube.Get(ctx, objectKey, credentialsSecret)
if err != nil {
return "", "", fmt.Errorf(errFetchSAKSecret, err)
}
username := credentialsSecret.Data["username"]
password := credentialsSecret.Data["password"]
if len(username) == 0 || len(password) == 0 {
return "", "", fmt.Errorf(errMissingSAK)
}
return string(username), string(password), nil
}
// NewDevice42Provider returns a reference to a new instance of a 'Device42' struct.
func NewDevice42Provider() *Device42 {
return &Device42{}
}
func (p *Device42) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Device42 == nil {
return nil, fmt.Errorf("no store type or wrong store type")
}
storeSpecDevice42 := storeSpec.Provider.Device42
cliStore := device42Client{
kube: kube,
store: storeSpecDevice42,
namespace: namespace,
storeKind: store.GetObjectKind().GroupVersionKind().Kind,
}
username, password, err := cliStore.getAuth(ctx)
if err != nil {
return nil, err
}
// Create a new client using credentials and options
p.client = NewAPI(storeSpecDevice42.Host, username, password, "443")
return p, nil
}
func (p *Device42) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
return false, fmt.Errorf(errNotImplemented)
}
func (p *Device42) Validate() (esv1beta1.ValidationResult, error) {
timeout := 15 * time.Second
url := fmt.Sprintf("https://%s:%s", p.client.(*API).baseURL, p.client.(*API).hostPort)
if err := utils.NetworkValidate(url, timeout); err != nil {
return esv1beta1.ValidationResultError, err
}
return esv1beta1.ValidationResultReady, nil
}
func (p *Device42) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
return fmt.Errorf(errNotImplemented)
}
func (p *Device42) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
return nil, fmt.Errorf(errNotImplemented)
}
func (p *Device42) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
return fmt.Errorf(errNotImplemented)
}
func (p *Device42) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
if utils.IsNil(p.client) {
return nil, fmt.Errorf(errUninitializedProvider)
}
data, err := p.client.GetSecret(ref.Key)
if err != nil {
return nil, err
}
return []byte(data.Password), nil
}
func (p *Device42) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
data, err := p.client.GetSecret(ref.Key)
if err != nil {
return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
}
return data.ToMap(), nil
}
func (p *Device42) Close(_ context.Context) error {
return nil
}
func init() {
esv1beta1.Register(&Device42{}, &esv1beta1.SecretStoreProvider{
Device42: &esv1beta1.Device42Provider{},
})
}

View file

@ -0,0 +1,130 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package device42
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
const (
DoRequestError = "error: do request: %w"
errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
)
type HTTPClient interface {
Do(*http.Request) (*http.Response, error)
}
type API struct {
client HTTPClient
baseURL string
hostPort string
password string
username string
}
type D42PasswordResponse struct {
Passwords []D42Password
}
type D42Password struct {
Password string `json:"password"`
ID int `json:"id"`
}
func NewAPI(baseURL, username, password, hostPort string) *API {
api := &API{
baseURL: baseURL,
hostPort: hostPort,
username: username,
password: password,
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
}
api.client = &http.Client{Transport: tr}
return api
}
func (api *API) doAuthenticatedRequest(r *http.Request) (*http.Response, error) {
r.SetBasicAuth(api.username, api.password)
return api.client.Do(r)
}
func ReadAndUnmarshal(resp *http.Response, target any) error {
var buf bytes.Buffer
defer func() {
err := resp.Body.Close()
if err != nil {
return
}
}()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("failed to authenticate with the given credentials: %d %s", resp.StatusCode, buf.String())
}
_, err := buf.ReadFrom(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(buf.Bytes(), target)
}
func (api *API) GetSecret(secretID string) (D42Password, error) {
// https://api.device42.com/#!/Passwords/getPassword
endpointURL := fmt.Sprintf("https://%s:%s/api/1.0/passwords/?id=%s&plain_text=yes", api.baseURL, api.hostPort, secretID)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
readSecretRequest, err := http.NewRequestWithContext(ctx, "GET", endpointURL, http.NoBody)
if err != nil {
return D42Password{}, fmt.Errorf("error: creating secrets request: %w", err)
}
respSecretRead, err := api.doAuthenticatedRequest(readSecretRequest) //nolint:bodyclose // linters bug
if err != nil {
return D42Password{}, fmt.Errorf(DoRequestError, err)
}
d42PasswordResponse := D42PasswordResponse{}
err = ReadAndUnmarshal(respSecretRead, &d42PasswordResponse)
if err != nil {
return D42Password{}, fmt.Errorf(errJSONSecretUnmarshal, err)
}
if len(d42PasswordResponse.Passwords) == 0 {
return D42Password{}, err
}
// There should only be one response
return d42PasswordResponse.Passwords[0], err
}
func (api *API) GetSecretMap(_ context.Context, _ esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return nil, fmt.Errorf(errNotImplemented)
}
func (s D42Password) ToMap() map[string][]byte {
m := make(map[string][]byte)
m["password"] = []byte(s.Password)
m["id"] = []byte(strconv.Itoa(s.ID))
return m
}

View file

@ -0,0 +1,127 @@
/*
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 device42
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
"testing"
fakedevice42 "github.com/external-secrets/external-secrets/pkg/provider/device42/fake"
)
const device42PasswordID = "12345"
func d42PasswordResponse() D42PasswordResponse {
return D42PasswordResponse{Passwords: []D42Password{d42Password()}}
}
func d42Password() D42Password {
return D42Password{
Password: "test_Password",
ID: 12345,
}
}
func TestDevice42ApiGetSecret(t *testing.T) {
type fields struct {
funcStack []func(req *http.Request) (*http.Response, error)
}
type args struct {
secretID string
}
tests := []struct {
name string
fields fields
args args
want D42Password
wantErr bool
}{
{
name: "get secret",
fields: fields{
funcStack: []func(req *http.Request) (*http.Response, error){
createResponder(d42PasswordResponse(), true), //nolint:bodyclose
},
},
args: args{
secretID: device42PasswordID,
},
want: d42Password(),
wantErr: false,
},
{
name: "bad response on secret entry",
fields: fields{
funcStack: []func(req *http.Request) (*http.Response, error){
createResponder([]byte("bad response body"), false), //nolint:bodyclose // linters bug
},
},
args: args{
secretID: device42PasswordID,
},
want: D42Password{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
api := &API{
client: &fakedevice42.MockClient{
FuncStack: tt.fields.funcStack,
},
baseURL: "localhost",
hostPort: "8714",
password: "test",
username: "test",
}
got, err := api.GetSecret(tt.args.secretID)
if (err != nil) != tt.wantErr {
t.Errorf("Device42.GetSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Device42.GetSecret() = %v, want %v", got, tt.want)
}
})
}
}
func createResponder(payload any, withMarshal bool) func(*http.Request) (*http.Response, error) {
return func(req *http.Request) (*http.Response, error) {
var payloadBytes []byte
if withMarshal {
payloadBytes, _ = json.Marshal(payload)
} else {
payloadBytes = payload.([]byte)
}
res := http.Response{
Status: "OK",
StatusCode: http.StatusOK,
Body: &closeableBuffer{bytes.NewReader(payloadBytes)},
}
return &res, nil
}
}
type closeableBuffer struct {
*bytes.Reader
}
func (cb *closeableBuffer) Close() error {
// Here you can add any cleanup code if needed
return nil
}

View file

@ -0,0 +1,31 @@
/*
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 "net/http"
// MockClient is the mock client.
type MockClient struct {
index int
FuncStack []func(req *http.Request) (*http.Response, error)
}
// Do is the mock client's `Do` func.
func (m *MockClient) Do(req *http.Request) (*http.Response, error) {
res, err := m.FuncStack[m.index](req)
m.index++
return res, err
}

View file

@ -24,6 +24,7 @@ import (
_ "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/device42"
_ "github.com/external-secrets/external-secrets/pkg/provider/doppler"
_ "github.com/external-secrets/external-secrets/pkg/provider/fake"
_ "github.com/external-secrets/external-secrets/pkg/provider/fortanix"