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:
parent
e01fc82ac2
commit
d29c001d37
17 changed files with 916 additions and 4 deletions
8
Makefile
8
Makefile
|
@ -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; \
|
||||
|
|
38
apis/externalsecrets/v1beta1/secretstore_device42_types.go
Normal file
38
apis/externalsecrets/v1beta1/secretstore_device42_types.go
Normal 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"`
|
||||
}
|
|
@ -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"`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
119
docs/api/spec.md
119
docs/api/spec.md
|
@ -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">
|
||||
|
|
|
@ -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
58
docs/provider/device42.md
Normal 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>
|
||||
```
|
16
docs/snippets/device42-external-secret.yaml
Normal file
16
docs/snippets/device42-external-secret.yaml
Normal 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"
|
|
@ -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
|
||||
|
|
182
pkg/provider/device42/device42.go
Normal file
182
pkg/provider/device42/device42.go
Normal 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{},
|
||||
})
|
||||
}
|
130
pkg/provider/device42/device42_api.go
Normal file
130
pkg/provider/device42/device42_api.go
Normal 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
|
||||
}
|
127
pkg/provider/device42/device42_api_test.go
Normal file
127
pkg/provider/device42/device42_api_test.go
Normal 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
|
||||
}
|
31
pkg/provider/device42/fake/fake.go
Normal file
31
pkg/provider/device42/fake/fake.go
Normal 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
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue