mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-15 17:51:01 +00:00
Infisical provider (#3477)
* feat: added crds for infisical provider Signed-off-by: = <akhilmhdh@gmail.com> * feat: implemented infisical provider logic Signed-off-by: = <akhilmhdh@gmail.com> * fix: resolved broken doc building due to vault doc error Signed-off-by: = <akhilmhdh@gmail.com> * docs: added doc for infisical provider Signed-off-by: = <akhilmhdh@gmail.com> * docs: fixed a warning in mkdocs on link Signed-off-by: = <akhilmhdh@gmail.com> * feat: resolved all lint issues Signed-off-by: = <akhilmhdh@gmail.com> * doc: removed k8s auth release banner from infisical doc Signed-off-by: = <akhilmhdh@gmail.com> * feat: added support for property to infisical provider Signed-off-by: = <akhilmhdh@gmail.com> * feat: removed auth type and made implicit ordering of authentication based on feedback Signed-off-by: = <akhilmhdh@gmail.com> * feat: support for referent authentication Signed-off-by: = <akhilmhdh@gmail.com> * feat: added error for tag not supported in find Signed-off-by: = <akhilmhdh@gmail.com> * fix: resolved failing build Signed-off-by: = <akhilmhdh@gmail.com> * feat: updated doc and added stability matrix for infisical Signed-off-by: = <akhilmhdh@gmail.com> * feat: switched to less error prone use and revoke token strategy and added validate interface logic Signed-off-by: = <akhilmhdh@gmail.com> * feat: code lint issue fixes Signed-off-by: = <akhilmhdh@gmail.com> * feat: resolved review comments for infisical client Signed-off-by: = <akhilmhdh@gmail.com> * feat: improved test cases and resolved sonar issues Signed-off-by: = <akhilmhdh@gmail.com> * feat: resolved sonar suggestions Signed-off-by: = <akhilmhdh@gmail.com> * feat: resolved sonar suggestions for test const ids Signed-off-by: = <akhilmhdh@gmail.com> * feat: store changes to assertError Signed-off-by: = <akhilmhdh@gmail.com> --------- Signed-off-by: = <akhilmhdh@gmail.com>
This commit is contained in:
parent
253fee4c3b
commit
ace1ff595f
24 changed files with 1818 additions and 72 deletions
53
apis/externalsecrets/v1beta1/secretsstore_infisical_types.go
Normal file
53
apis/externalsecrets/v1beta1/secretsstore_infisical_types.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UniversalAuthCredentials struct {
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
ClientID esmeta.SecretKeySelector `json:"clientId"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
ClientSecret esmeta.SecretKeySelector `json:"clientSecret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfisicalAuth struct {
|
||||||
|
// +optional
|
||||||
|
UniversalAuthCredentials *UniversalAuthCredentials `json:"universalAuthCredentials,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineIdentityScopeInWorkspace struct {
|
||||||
|
// +kubebuilder:default="/"
|
||||||
|
// +optional
|
||||||
|
SecretsPath string `json:"secretsPath,omitempty"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
EnvironmentSlug string `json:"environmentSlug"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
ProjectSlug string `json:"projectSlug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfisicalProvider configures a store to sync secrets using the Infisical provider.
|
||||||
|
type InfisicalProvider struct {
|
||||||
|
// Auth configures how the Operator authenticates with the Infisical API
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
Auth InfisicalAuth `json:"auth"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"`
|
||||||
|
// +kubebuilder:default="https://app.infisical.com/api"
|
||||||
|
// +optional
|
||||||
|
HostAPI string `json:"hostAPI,omitempty"`
|
||||||
|
}
|
|
@ -163,6 +163,10 @@ type SecretStoreProvider struct {
|
||||||
|
|
||||||
// +optional
|
// +optional
|
||||||
Passbolt *PassboltProvider `json:"passbolt,omitempty"`
|
Passbolt *PassboltProvider `json:"passbolt,omitempty"`
|
||||||
|
|
||||||
|
// Infisical configures this store to sync secrets using the Infisical provider
|
||||||
|
// +optional
|
||||||
|
Infisical *InfisicalProvider `json:"infisical,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CAProviderType string
|
type CAProviderType string
|
||||||
|
|
|
@ -1669,6 +1669,43 @@ func (in *IBMProvider) DeepCopy() *IBMProvider {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *InfisicalAuth) DeepCopyInto(out *InfisicalAuth) {
|
||||||
|
*out = *in
|
||||||
|
if in.UniversalAuthCredentials != nil {
|
||||||
|
in, out := &in.UniversalAuthCredentials, &out.UniversalAuthCredentials
|
||||||
|
*out = new(UniversalAuthCredentials)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalAuth.
|
||||||
|
func (in *InfisicalAuth) DeepCopy() *InfisicalAuth {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(InfisicalAuth)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *InfisicalProvider) DeepCopyInto(out *InfisicalProvider) {
|
||||||
|
*out = *in
|
||||||
|
in.Auth.DeepCopyInto(&out.Auth)
|
||||||
|
out.SecretsScope = in.SecretsScope
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalProvider.
|
||||||
|
func (in *InfisicalProvider) DeepCopy() *InfisicalProvider {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(InfisicalProvider)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *KeeperSecurityProvider) DeepCopyInto(out *KeeperSecurityProvider) {
|
func (in *KeeperSecurityProvider) DeepCopyInto(out *KeeperSecurityProvider) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -1757,6 +1794,21 @@ func (in *KubernetesServer) DeepCopy() *KubernetesServer {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *MachineIdentityScopeInWorkspace) DeepCopyInto(out *MachineIdentityScopeInWorkspace) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineIdentityScopeInWorkspace.
|
||||||
|
func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWorkspace {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MachineIdentityScopeInWorkspace)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *NoSecretError) DeepCopyInto(out *NoSecretError) {
|
func (in *NoSecretError) DeepCopyInto(out *NoSecretError) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -2305,6 +2357,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
|
||||||
*out = new(PassboltProvider)
|
*out = new(PassboltProvider)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.Infisical != nil {
|
||||||
|
in, out := &in.Infisical, &out.Infisical
|
||||||
|
*out = new(InfisicalProvider)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
|
||||||
|
@ -2616,6 +2673,23 @@ func (in *TokenAuth) DeepCopy() *TokenAuth {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *UniversalAuthCredentials) DeepCopyInto(out *UniversalAuthCredentials) {
|
||||||
|
*out = *in
|
||||||
|
in.ClientID.DeepCopyInto(&out.ClientID)
|
||||||
|
in.ClientSecret.DeepCopyInto(&out.ClientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UniversalAuthCredentials.
|
||||||
|
func (in *UniversalAuthCredentials) DeepCopy() *UniversalAuthCredentials {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(UniversalAuthCredentials)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *VaultAppRole) DeepCopyInto(out *VaultAppRole) {
|
func (in *VaultAppRole) DeepCopyInto(out *VaultAppRole) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
|
@ -2883,6 +2883,81 @@ spec:
|
||||||
required:
|
required:
|
||||||
- auth
|
- auth
|
||||||
type: object
|
type: object
|
||||||
|
infisical:
|
||||||
|
description: Infisical configures this store to sync secrets using
|
||||||
|
the Infisical provider
|
||||||
|
properties:
|
||||||
|
auth:
|
||||||
|
description: Auth configures how the Operator authenticates
|
||||||
|
with the Infisical API
|
||||||
|
properties:
|
||||||
|
universalAuthCredentials:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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
|
||||||
|
clientSecret:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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:
|
||||||
|
- clientId
|
||||||
|
- clientSecret
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
hostAPI:
|
||||||
|
default: https://app.infisical.com/api
|
||||||
|
type: string
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
environmentSlug:
|
||||||
|
type: string
|
||||||
|
projectSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
default: /
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- environmentSlug
|
||||||
|
- projectSlug
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- auth
|
||||||
|
- secretsScope
|
||||||
|
type: object
|
||||||
keepersecurity:
|
keepersecurity:
|
||||||
description: KeeperSecurity configures this store to sync secrets
|
description: KeeperSecurity configures this store to sync secrets
|
||||||
using the KeeperSecurity provider
|
using the KeeperSecurity provider
|
||||||
|
|
|
@ -2883,6 +2883,81 @@ spec:
|
||||||
required:
|
required:
|
||||||
- auth
|
- auth
|
||||||
type: object
|
type: object
|
||||||
|
infisical:
|
||||||
|
description: Infisical configures this store to sync secrets using
|
||||||
|
the Infisical provider
|
||||||
|
properties:
|
||||||
|
auth:
|
||||||
|
description: Auth configures how the Operator authenticates
|
||||||
|
with the Infisical API
|
||||||
|
properties:
|
||||||
|
universalAuthCredentials:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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
|
||||||
|
clientSecret:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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:
|
||||||
|
- clientId
|
||||||
|
- clientSecret
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
hostAPI:
|
||||||
|
default: https://app.infisical.com/api
|
||||||
|
type: string
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
environmentSlug:
|
||||||
|
type: string
|
||||||
|
projectSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
default: /
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- environmentSlug
|
||||||
|
- projectSlug
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- auth
|
||||||
|
- secretsScope
|
||||||
|
type: object
|
||||||
keepersecurity:
|
keepersecurity:
|
||||||
description: KeeperSecurity configures this store to sync secrets
|
description: KeeperSecurity configures this store to sync secrets
|
||||||
using the KeeperSecurity provider
|
using the KeeperSecurity provider
|
||||||
|
|
|
@ -3337,6 +3337,77 @@ spec:
|
||||||
required:
|
required:
|
||||||
- auth
|
- auth
|
||||||
type: object
|
type: object
|
||||||
|
infisical:
|
||||||
|
description: Infisical configures this store to sync secrets using the Infisical provider
|
||||||
|
properties:
|
||||||
|
auth:
|
||||||
|
description: Auth configures how the Operator authenticates with the Infisical API
|
||||||
|
properties:
|
||||||
|
universalAuthCredentials:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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
|
||||||
|
clientSecret:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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:
|
||||||
|
- clientId
|
||||||
|
- clientSecret
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
hostAPI:
|
||||||
|
default: https://app.infisical.com/api
|
||||||
|
type: string
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
environmentSlug:
|
||||||
|
type: string
|
||||||
|
projectSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
default: /
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- environmentSlug
|
||||||
|
- projectSlug
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- auth
|
||||||
|
- secretsScope
|
||||||
|
type: object
|
||||||
keepersecurity:
|
keepersecurity:
|
||||||
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
|
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
|
||||||
properties:
|
properties:
|
||||||
|
@ -8712,6 +8783,77 @@ spec:
|
||||||
required:
|
required:
|
||||||
- auth
|
- auth
|
||||||
type: object
|
type: object
|
||||||
|
infisical:
|
||||||
|
description: Infisical configures this store to sync secrets using the Infisical provider
|
||||||
|
properties:
|
||||||
|
auth:
|
||||||
|
description: Auth configures how the Operator authenticates with the Infisical API
|
||||||
|
properties:
|
||||||
|
universalAuthCredentials:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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
|
||||||
|
clientSecret:
|
||||||
|
description: |-
|
||||||
|
A reference to a specific 'key' within a Secret resource,
|
||||||
|
In some instances, `key` is a required field.
|
||||||
|
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:
|
||||||
|
- clientId
|
||||||
|
- clientSecret
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
hostAPI:
|
||||||
|
default: https://app.infisical.com/api
|
||||||
|
type: string
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
environmentSlug:
|
||||||
|
type: string
|
||||||
|
projectSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
default: /
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- environmentSlug
|
||||||
|
- projectSlug
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- auth
|
||||||
|
- secretsScope
|
||||||
|
type: object
|
||||||
keepersecurity:
|
keepersecurity:
|
||||||
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
|
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
|
||||||
properties:
|
properties:
|
||||||
|
|
191
docs/api/spec.md
191
docs/api/spec.md
|
@ -4372,6 +4372,92 @@ string
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h3 id="external-secrets.io/v1beta1.InfisicalAuth">InfisicalAuth
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
(<em>Appears on:</em>
|
||||||
|
<a href="#external-secrets.io/v1beta1.InfisicalProvider">InfisicalProvider</a>)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>universalAuthCredentials</code></br>
|
||||||
|
<em>
|
||||||
|
<a href="#external-secrets.io/v1beta1.UniversalAuthCredentials">
|
||||||
|
UniversalAuthCredentials
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3 id="external-secrets.io/v1beta1.InfisicalProvider">InfisicalProvider
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
(<em>Appears on:</em>
|
||||||
|
<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<p>InfisicalProvider configures a store to sync secrets using the Infisical provider.</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.InfisicalAuth">
|
||||||
|
InfisicalAuth
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p>Auth configures how the Operator authenticates with the Infisical API</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>secretsScope</code></br>
|
||||||
|
<em>
|
||||||
|
<a href="#external-secrets.io/v1beta1.MachineIdentityScopeInWorkspace">
|
||||||
|
MachineIdentityScopeInWorkspace
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>hostAPI</code></br>
|
||||||
|
<em>
|
||||||
|
string
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<h3 id="external-secrets.io/v1beta1.KeeperSecurityProvider">KeeperSecurityProvider
|
<h3 id="external-secrets.io/v1beta1.KeeperSecurityProvider">KeeperSecurityProvider
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
|
@ -4586,6 +4672,55 @@ CAProvider
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h3 id="external-secrets.io/v1beta1.MachineIdentityScopeInWorkspace">MachineIdentityScopeInWorkspace
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
(<em>Appears on:</em>
|
||||||
|
<a href="#external-secrets.io/v1beta1.InfisicalProvider">InfisicalProvider</a>)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>secretsPath</code></br>
|
||||||
|
<em>
|
||||||
|
string
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>environmentSlug</code></br>
|
||||||
|
<em>
|
||||||
|
string
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>projectSlug</code></br>
|
||||||
|
<em>
|
||||||
|
string
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<h3 id="external-secrets.io/v1beta1.NoSecretError">NoSecretError
|
<h3 id="external-secrets.io/v1beta1.NoSecretError">NoSecretError
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
|
@ -6055,6 +6190,20 @@ PassboltProvider
|
||||||
<em>(Optional)</em>
|
<em>(Optional)</em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>infisical</code></br>
|
||||||
|
<em>
|
||||||
|
<a href="#external-secrets.io/v1beta1.InfisicalProvider">
|
||||||
|
InfisicalProvider
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>Infisical configures this store to sync secrets using the Infisical provider</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef
|
<h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef
|
||||||
|
@ -6932,6 +7081,48 @@ External Secrets meta/v1.SecretKeySelector
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h3 id="external-secrets.io/v1beta1.UniversalAuthCredentials">UniversalAuthCredentials
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
(<em>Appears on:</em>
|
||||||
|
<a href="#external-secrets.io/v1beta1.InfisicalAuth">InfisicalAuth</a>)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>clientId</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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>clientSecret</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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<h3 id="external-secrets.io/v1beta1.ValidationResult">ValidationResult
|
<h3 id="external-secrets.io/v1beta1.ValidationResult">ValidationResult
|
||||||
(<code>byte</code> alias)</p></h3>
|
(<code>byte</code> alias)</p></h3>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -55,6 +55,7 @@ The following table describes the stability level of each provider and who's res
|
||||||
| [Delinea](https://external-secrets.io/latest/provider/delinea) | alpha | [@michaelsauter](https://github.com/michaelsauter/) |
|
| [Delinea](https://external-secrets.io/latest/provider/delinea) | alpha | [@michaelsauter](https://github.com/michaelsauter/) |
|
||||||
| [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi) | alpha | [@dirien](https://github.com/dirien) |
|
| [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi) | alpha | [@dirien](https://github.com/dirien) |
|
||||||
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
|
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
|
||||||
|
| [Infisical](https://external-secrets.io/latest/provider/infisical) | alpha | [@akhilmhdh](https://github.com/akhilmhdh) |
|
||||||
|
|
||||||
## Provider Feature Support
|
## Provider Feature Support
|
||||||
|
|
||||||
|
@ -84,6 +85,7 @@ The following table show the support for features across different providers.
|
||||||
| Delinea | x | | | | x | | |
|
| Delinea | x | | | | x | | |
|
||||||
| Pulumi ESC | x | | | | x | | |
|
| Pulumi ESC | x | | | | x | | |
|
||||||
| Passbolt | x | | | | x | | |
|
| Passbolt | x | | | | x | | |
|
||||||
|
| Infisical | x | | | x | x | | |
|
||||||
|
|
||||||
## Support Policy
|
## Support Policy
|
||||||
|
|
||||||
|
|
BIN
docs/pictures/external-secrets-operator.png
Normal file
BIN
docs/pictures/external-secrets-operator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 215 KiB |
|
@ -364,7 +364,7 @@ set of AWS Programmatic access credentials stored in a `Kind=Secret` and referen
|
||||||
|
|
||||||
### Mutual authentication (mTLS)
|
### Mutual authentication (mTLS)
|
||||||
|
|
||||||
Under specific compliance requirements, the Vault server can be set up to enforce mutual authentication from clients across all APIs by configuring the server with `tls_require_and_verify_client_cert = true`. This configuration differs fundamentally from the [TLS certificates auth method](#TLS-certificates-authentication). While the TLS certificates auth method allows the issuance of a Vault token through the `/v1/auth/cert/login` API, the mTLS configuration solely focuses on TLS transport layer authentication and lacks any authorization-related capabilities. It's important to note that the Vault token must still be included in the request, following any of the supported authentication methods mentioned earlier.
|
Under specific compliance requirements, the Vault server can be set up to enforce mutual authentication from clients across all APIs by configuring the server with `tls_require_and_verify_client_cert = true`. This configuration differs fundamentally from the [TLS certificates auth method](#tls-certificates-authentication). While the TLS certificates auth method allows the issuance of a Vault token through the `/v1/auth/cert/login` API, the mTLS configuration solely focuses on TLS transport layer authentication and lacks any authorization-related capabilities. It's important to note that the Vault token must still be included in the request, following any of the supported authentication methods mentioned earlier.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
{% include 'vault-mtls-store.yaml' %}
|
{% include 'vault-mtls-store.yaml' %}
|
||||||
|
|
68
docs/provider/infisical.md
Normal file
68
docs/provider/infisical.md
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
![Infisical k8s Diagram](../pictures/external-secrets-operator.png)
|
||||||
|
|
||||||
|
Sync secrets from [Infisical](https://www.infisical.com) to your Kubernetes cluster using External Secrets Operator.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
In order for the operator to fetch secrets from Infisical, it needs to first authenticate with Infisical.
|
||||||
|
|
||||||
|
To authenticate, you can use [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) from [Machine identities](https://infisical.com/docs/documentation/platform/identities/machine-identities).
|
||||||
|
|
||||||
|
Follow the [guide here](https://infisical.com/docs/documentation/platform/identities/universal-auth) to learn how to create and obtain a pair of Client Secret and Client ID.
|
||||||
|
|
||||||
|
## Storing Your Machine Identity Secrets
|
||||||
|
|
||||||
|
Once you have generated a pair of `Client ID` and `Client Secret`, you will need to store these credentials in your cluster as a Kubernetes secret.
|
||||||
|
|
||||||
|
!!! note inline end
|
||||||
|
Remember to replace with your own Machine Identity credentials.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: universal-auth-credentials
|
||||||
|
type: Opaque
|
||||||
|
|
||||||
|
stringData:
|
||||||
|
clientId: <machine identity client id>
|
||||||
|
clientSecret: <machine identity client secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secret Store
|
||||||
|
|
||||||
|
You will then need to create a generic `SecretStore`. An sample `SecretStore` has been is shown below.
|
||||||
|
|
||||||
|
!!! tip inline end
|
||||||
|
To get your project slug from Infisical, head over to the project settings and click the button `Copy Project Slug`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{% include 'infisical-generic-secret-store.yaml' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
For `ClusterSecretStore`, be sure to set `namespace` in `universalAuthCredentials.clientId` and `universalAuthCredentials.clientSecret`.
|
||||||
|
|
||||||
|
## Fetch Individual Secret(s)
|
||||||
|
|
||||||
|
To sync one or more secrets individually, use the following YAML:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{% include 'infisical-fetch-secret.yaml' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fetch All Secrets
|
||||||
|
|
||||||
|
To sync all secrets from an Infisical , use the following YAML:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
{% include 'infisical-fetch-all-secrets.yaml' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filter By Prefix/Name
|
||||||
|
|
||||||
|
To filter secrets by `path` (path prefix) and `name` (regular expression).
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
{% include 'infisical-filtered-secrets.yaml' %}
|
||||||
|
```
|
||||||
|
|
16
docs/snippets/infisical-fetch-all-secrets.yaml
Normal file
16
docs/snippets/infisical-fetch-all-secrets.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: infisical-managed-secrets
|
||||||
|
spec:
|
||||||
|
secretStoreRef:
|
||||||
|
kind: SecretStore
|
||||||
|
name: infisical
|
||||||
|
|
||||||
|
target:
|
||||||
|
name: auth-api
|
||||||
|
|
||||||
|
dataFrom:
|
||||||
|
- find:
|
||||||
|
name:
|
||||||
|
regexp: .*
|
16
docs/snippets/infisical-fetch-secret.yaml
Normal file
16
docs/snippets/infisical-fetch-secret.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: infisical-managed-secrets
|
||||||
|
spec:
|
||||||
|
secretStoreRef:
|
||||||
|
kind: SecretStore
|
||||||
|
name: infisical
|
||||||
|
|
||||||
|
target:
|
||||||
|
name: auth-api
|
||||||
|
|
||||||
|
data:
|
||||||
|
- secretKey: API_KEY
|
||||||
|
remoteRef:
|
||||||
|
key: API_KEY
|
15
docs/snippets/infisical-filtered-secrets.yaml
Normal file
15
docs/snippets/infisical-filtered-secrets.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: infisical-managed-secrets
|
||||||
|
spec:
|
||||||
|
secretStoreRef:
|
||||||
|
kind: SecretStore
|
||||||
|
name: infisical
|
||||||
|
|
||||||
|
target:
|
||||||
|
name: auth-api
|
||||||
|
|
||||||
|
dataFrom:
|
||||||
|
- find:
|
||||||
|
path: DB_
|
25
docs/snippets/infisical-generic-secret-store.yaml
Normal file
25
docs/snippets/infisical-generic-secret-store.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: SecretStore
|
||||||
|
metadata:
|
||||||
|
name: infisical
|
||||||
|
spec:
|
||||||
|
provider:
|
||||||
|
infisical:
|
||||||
|
auth:
|
||||||
|
universalAuthCredentials:
|
||||||
|
clientId:
|
||||||
|
key: clientId
|
||||||
|
namespace: default
|
||||||
|
name: universal-auth-credentials
|
||||||
|
clientSecret:
|
||||||
|
key: clientSecret
|
||||||
|
namespace: default
|
||||||
|
name: universal-auth-credentials
|
||||||
|
# Details to pull secrets from
|
||||||
|
secretsScope:
|
||||||
|
projectSlug: first-project-fujo
|
||||||
|
environmentSlug: dev # "dev", "staging", "prod", etc..
|
||||||
|
# optional
|
||||||
|
secretsPath: / # Root is "/"
|
||||||
|
# optional
|
||||||
|
hostAPI: https://app.infisical.com
|
|
@ -119,6 +119,7 @@ nav:
|
||||||
- Onboardbase: provider/onboardbase.md
|
- Onboardbase: provider/onboardbase.md
|
||||||
- Password Depot: provider-passworddepot.md
|
- Password Depot: provider-passworddepot.md
|
||||||
- Fortanix: provider/fortanix.md
|
- Fortanix: provider/fortanix.md
|
||||||
|
- Infisical: provider/infisical.md
|
||||||
- Examples:
|
- Examples:
|
||||||
- FluxCD: examples/gitops-using-fluxcd.md
|
- FluxCD: examples/gitops-using-fluxcd.md
|
||||||
- Anchore Engine: examples/anchore-engine-credentials.md
|
- Anchore Engine: examples/anchore-engine-credentials.md
|
||||||
|
|
257
pkg/provider/infisical/api/api.go
Normal file
257
pkg/provider/infisical/api/api.go
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
/*
|
||||||
|
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 impliec.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/metrics"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InfisicalClient struct {
|
||||||
|
BaseURL *url.URL
|
||||||
|
client *http.Client
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfisicalApis interface {
|
||||||
|
MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error)
|
||||||
|
GetSecretsV3(data GetSecretsV3Request) (map[string]string, error)
|
||||||
|
GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error)
|
||||||
|
RevokeAccessToken() error
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserAgentName = "k8-external-secrets-operator"
|
||||||
|
const errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
|
||||||
|
|
||||||
|
func NewAPIClient(baseURL string) (*InfisicalClient, error) {
|
||||||
|
baseParsedURL, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
api := &InfisicalClient{
|
||||||
|
BaseURL: baseParsedURL,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: time.Second * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return api, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) SetTokenViaMachineIdentity(clientID, clientSecret string) error {
|
||||||
|
if a.token != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loginResponse, err := a.MachineIdentityLoginViaUniversalAuth(MachineIdentityUniversalAuthLoginRequest{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.token = loginResponse.AccessToken
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) RevokeAccessToken() error {
|
||||||
|
if a.token == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := a.RevokeMachineIdentityAccessToken(RevokeMachineIdentityAccessTokenRequest{AccessToken: a.token}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.token = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) resolveEndpoint(path string) string {
|
||||||
|
return a.BaseURL.ResolveReference(&url.URL{Path: path}).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) do(r *http.Request) (*http.Response, error) {
|
||||||
|
if a.token != "" {
|
||||||
|
r.Header.Add("Authorization", "Bearer "+a.token)
|
||||||
|
}
|
||||||
|
r.Header.Add("User-Agent", UserAgentName)
|
||||||
|
r.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return a.client.Do(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error) {
|
||||||
|
endpointURL := a.resolveEndpoint("api/v1/auth/universal-auth/login")
|
||||||
|
body, err := MarshalReqBody(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, endpointURL, body)
|
||||||
|
metrics.ObserveAPICall(constants.ProviderName, "MachineIdentityLoginViaUniversalAuth", err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawRes, err := a.do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res MachineIdentityDetailsResponse
|
||||||
|
err = ReadAndUnmarshal(rawRes, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
|
||||||
|
}
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) RevokeMachineIdentityAccessToken(data RevokeMachineIdentityAccessTokenRequest) (*RevokeMachineIdentityAccessTokenResponse, error) {
|
||||||
|
endpointURL := a.resolveEndpoint("api/v1/auth/token/revoke")
|
||||||
|
body, err := MarshalReqBody(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, endpointURL, body)
|
||||||
|
metrics.ObserveAPICall(constants.ProviderName, "RevokeMachineIdentityAccessToken", err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawRes, err := a.do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res RevokeMachineIdentityAccessTokenResponse
|
||||||
|
err = ReadAndUnmarshal(rawRes, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
|
||||||
|
}
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) GetSecretsV3(data GetSecretsV3Request) (map[string]string, error) {
|
||||||
|
endpointURL := a.resolveEndpoint("api/v3/secrets/raw")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, endpointURL, http.NoBody)
|
||||||
|
metrics.ObserveAPICall(constants.ProviderName, "GetSecretsV3", err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("workspaceSlug", data.ProjectSlug)
|
||||||
|
q.Add("environment", data.EnvironmentSlug)
|
||||||
|
q.Add("secretPath", data.SecretPath)
|
||||||
|
q.Add("include_imports", "true")
|
||||||
|
q.Add("expandSecretReferences", "true")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
rawRes, err := a.do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res GetSecretsV3Response
|
||||||
|
err = ReadAndUnmarshal(rawRes, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets := make(map[string]string)
|
||||||
|
for _, s := range res.ImportedSecrets {
|
||||||
|
for _, el := range s.Secrets {
|
||||||
|
secrets[el.SecretKey] = el.SecretValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, el := range res.Secrets {
|
||||||
|
secrets[el.SecretKey] = el.SecretValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InfisicalClient) GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error) {
|
||||||
|
endpointURL := a.resolveEndpoint(fmt.Sprintf("api/v3/secrets/raw/%s", data.SecretKey))
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, endpointURL, http.NoBody)
|
||||||
|
metrics.ObserveAPICall(constants.ProviderName, "GetSecretByKeyV3", err)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("workspaceSlug", data.ProjectSlug)
|
||||||
|
q.Add("environment", data.EnvironmentSlug)
|
||||||
|
q.Add("secretPath", data.SecretPath)
|
||||||
|
q.Add("include_imports", "true")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
rawRes, err := a.do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if rawRes.StatusCode == 400 {
|
||||||
|
var errRes InfisicalAPIErrorResponse
|
||||||
|
err = ReadAndUnmarshal(rawRes, &errRes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf(errJSONSecretUnmarshal, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errRes.Message == "Secret not found" {
|
||||||
|
return "", esv1beta1.NoSecretError{}
|
||||||
|
}
|
||||||
|
return "", errors.New(errRes.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res GetSecretByKeyV3Response
|
||||||
|
err = ReadAndUnmarshal(rawRes, &res)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf(errJSONSecretUnmarshal, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Secret.SecretValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalReqBody(data any) (*bytes.Reader, error) {
|
||||||
|
body, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes.NewReader(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAndUnmarshal(resp *http.Response, target any) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
defer resp.Body.Close()
|
||||||
|
_, err := buf.ReadFrom(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(buf.Bytes(), target)
|
||||||
|
}
|
87
pkg/provider/infisical/api/api_models.go
Normal file
87
pkg/provider/infisical/api/api_models.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
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 impliec.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
type MachineIdentityUniversalAuthRefreshRequest struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineIdentityDetailsResponse struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
ExpiresIn int `json:"expiresIn"`
|
||||||
|
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
|
||||||
|
TokenType string `json:"tokenType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineIdentityUniversalAuthLoginRequest struct {
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
ClientSecret string `json:"clientSecret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RevokeMachineIdentityAccessTokenRequest struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RevokeMachineIdentityAccessTokenResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSecretByKeyV3Request struct {
|
||||||
|
EnvironmentSlug string `json:"environment"`
|
||||||
|
ProjectSlug string `json:"workspaceSlug"`
|
||||||
|
SecretPath string `json:"secretPath"`
|
||||||
|
SecretKey string `json:"secretKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSecretByKeyV3Response struct {
|
||||||
|
Secret SecretsV3 `json:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSecretsV3Request struct {
|
||||||
|
EnvironmentSlug string `json:"environment"`
|
||||||
|
ProjectSlug string `json:"workspaceSlug"`
|
||||||
|
SecretPath string `json:"secretPath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSecretsV3Response struct {
|
||||||
|
Secrets []SecretsV3 `json:"secrets"`
|
||||||
|
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
|
||||||
|
Modified bool `json:"modified,omitempty"`
|
||||||
|
ETag string `json:"ETag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecretsV3 struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Workspace string `json:"workspace"`
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Type string `json:"string"`
|
||||||
|
SecretKey string `json:"secretKey"`
|
||||||
|
SecretValue string `json:"secretValue"`
|
||||||
|
SecretComment string `json:"secretComment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportedSecretV3 struct {
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
FolderID string `json:"folderId"`
|
||||||
|
SecretPath string `json:"secretPath"`
|
||||||
|
Secrets []SecretsV3 `json:"secrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfisicalAPIErrorResponse struct {
|
||||||
|
StatusCode int `json:"statusCode"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Error any `json:"error"`
|
||||||
|
}
|
170
pkg/provider/infisical/client.go
Normal file
170
pkg/provider/infisical/client.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
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 impliec.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package infisical
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
|
||||||
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/find"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotImplemented = errors.New("not implemented")
|
||||||
|
errPropertyNotFound = "property %s does not exist in secret %s"
|
||||||
|
errTagsNotImplemented = errors.New("find by tags not supported")
|
||||||
|
)
|
||||||
|
|
||||||
|
func getPropertyValue(jsonData, propertyName, keyName string) ([]byte, error) {
|
||||||
|
result := gjson.Get(jsonData, propertyName)
|
||||||
|
if !result.Exists() {
|
||||||
|
return nil, fmt.Errorf(errPropertyNotFound, propertyName, keyName)
|
||||||
|
}
|
||||||
|
return []byte(result.Str), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if GetSecret returns an error with type NoSecretError.
|
||||||
|
// then the secret entry will be deleted depending on the deletionPolicy.
|
||||||
|
func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||||
|
secret, err := p.apiClient.GetSecretByKeyV3(api.GetSecretByKeyV3Request{
|
||||||
|
EnvironmentSlug: p.apiScope.EnvironmentSlug,
|
||||||
|
ProjectSlug: p.apiScope.ProjectSlug,
|
||||||
|
SecretPath: p.apiScope.SecretPath,
|
||||||
|
SecretKey: ref.Key,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ref.Property != "" {
|
||||||
|
propertyValue, err := getPropertyValue(secret, ref.Property, ref.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return propertyValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecretMap returns multiple k/v pairs from the provider.
|
||||||
|
func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||||
|
secret, err := p.GetSecret(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv := make(map[string]json.RawMessage)
|
||||||
|
err = json.Unmarshal(secret, &kv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
|
||||||
|
}
|
||||||
|
secretData := make(map[string][]byte)
|
||||||
|
for k, v := range kv {
|
||||||
|
var strVal string
|
||||||
|
err = json.Unmarshal(v, &strVal)
|
||||||
|
if err == nil {
|
||||||
|
secretData[k] = []byte(strVal)
|
||||||
|
} else {
|
||||||
|
secretData[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secretData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSecrets returns multiple k/v pairs from the provider.
|
||||||
|
func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
|
||||||
|
if ref.Tags != nil {
|
||||||
|
return nil, errTagsNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
|
||||||
|
EnvironmentSlug: p.apiScope.EnvironmentSlug,
|
||||||
|
ProjectSlug: p.apiScope.ProjectSlug,
|
||||||
|
SecretPath: p.apiScope.SecretPath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secretMap := make(map[string][]byte)
|
||||||
|
for key, value := range secrets {
|
||||||
|
secretMap[key] = []byte(value)
|
||||||
|
}
|
||||||
|
if ref.Name == nil && ref.Path == nil {
|
||||||
|
return secretMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var matcher *find.Matcher
|
||||||
|
if ref.Name != nil {
|
||||||
|
m, err := find.New(*ref.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
matcher = m
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := map[string][]byte{}
|
||||||
|
for key, value := range secrets {
|
||||||
|
if (matcher != nil && !matcher.MatchName(key)) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
selected[key] = []byte(value)
|
||||||
|
}
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the client is configured correctly.
|
||||||
|
// and is able to retrieve secrets from the provider.
|
||||||
|
// If the validation result is unknown it will be ignored.
|
||||||
|
func (p *Provider) Validate() (esv1beta1.ValidationResult, error) {
|
||||||
|
// try to fetch the secrets to ensure provided credentials has access to read secrets
|
||||||
|
_, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
|
||||||
|
EnvironmentSlug: p.apiScope.EnvironmentSlug,
|
||||||
|
ProjectSlug: p.apiScope.ProjectSlug,
|
||||||
|
SecretPath: p.apiScope.SecretPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return esv1beta1.ValidationResultError, fmt.Errorf("cannot read secrets with provided project scope project:%s environment:%s secret-path:%s, %w", p.apiScope.ProjectSlug, p.apiScope.EnvironmentSlug, p.apiScope.SecretPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return esv1beta1.ValidationResultReady, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushSecret will write a single secret into the provider.
|
||||||
|
func (p *Provider) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
|
||||||
|
return errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSecret will delete the secret from a provider.
|
||||||
|
func (p *Provider) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
|
||||||
|
return errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretExists checks if a secret is already present in the provider at the given location.
|
||||||
|
func (p *Provider) SecretExists(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) (bool, error) {
|
||||||
|
return false, errNotImplemented
|
||||||
|
}
|
19
pkg/provider/infisical/constants/constants.go
Normal file
19
pkg/provider/infisical/constants/constants.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
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 impliec.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
UniversalAuth = "universal-auth"
|
||||||
|
ProviderName = "infisical"
|
||||||
|
)
|
58
pkg/provider/infisical/fake/fake.go
Normal file
58
pkg/provider/infisical/fake/fake.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
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 impliec.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingMockImplementation = errors.New("missing mock implmentation")
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockInfisicalClient struct {
|
||||||
|
MockedGetSecretV3 func(data api.GetSecretsV3Request) (map[string]string, error)
|
||||||
|
MockedGetSecretByKeyV3 func(data api.GetSecretByKeyV3Request) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MockInfisicalClient) MachineIdentityLoginViaUniversalAuth(data api.MachineIdentityUniversalAuthLoginRequest) (*api.MachineIdentityDetailsResponse, error) {
|
||||||
|
return &api.MachineIdentityDetailsResponse{
|
||||||
|
AccessToken: "test-access-token",
|
||||||
|
ExpiresIn: int(time.Hour * 24),
|
||||||
|
TokenType: "bearer",
|
||||||
|
AccessTokenMaxTTL: int(time.Hour * 24 * 2),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MockInfisicalClient) GetSecretsV3(data api.GetSecretsV3Request) (map[string]string, error) {
|
||||||
|
if a.MockedGetSecretV3 == nil {
|
||||||
|
return nil, ErrMissingMockImplementation
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.MockedGetSecretV3(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MockInfisicalClient) GetSecretByKeyV3(data api.GetSecretByKeyV3Request) (string, error) {
|
||||||
|
if a.MockedGetSecretByKeyV3 == nil {
|
||||||
|
return "", ErrMissingMockImplementation
|
||||||
|
}
|
||||||
|
return a.MockedGetSecretByKeyV3(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MockInfisicalClient) RevokeAccessToken() error {
|
||||||
|
return nil
|
||||||
|
}
|
159
pkg/provider/infisical/provider.go
Normal file
159
pkg/provider/infisical/provider.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
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 implieclient.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package infisical
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
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"
|
||||||
|
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Logger = ctrl.Log.WithName("provider").WithName(constants.ProviderName)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
apiClient api.InfisicalApis
|
||||||
|
apiScope *InfisicalClientScope
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfisicalClientScope struct {
|
||||||
|
SecretPath string
|
||||||
|
ProjectSlug string
|
||||||
|
EnvironmentSlug string
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/external-secrets/external-secrets/issues/644
|
||||||
|
var _ esv1beta1.SecretsClient = &Provider{}
|
||||||
|
var _ esv1beta1.Provider = &Provider{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
|
||||||
|
Infisical: &esv1beta1.InfisicalProvider{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
|
||||||
|
return esv1beta1.SecretStoreReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) 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.Infisical == nil {
|
||||||
|
return nil, errors.New("invalid infisical store")
|
||||||
|
}
|
||||||
|
|
||||||
|
infisicalSpec := storeSpec.Provider.Infisical
|
||||||
|
|
||||||
|
apiClient, err := api.NewAPIClient(infisicalSpec.HostAPI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if infisicalSpec.Auth.UniversalAuthCredentials != nil {
|
||||||
|
universalAuthCredentials := infisicalSpec.Auth.UniversalAuthCredentials
|
||||||
|
clientID, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSecret, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apiClient.SetTokenViaMachineIdentity(clientID, clientSecret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to authenticate via universal auth %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Provider{
|
||||||
|
apiClient: apiClient,
|
||||||
|
apiScope: &InfisicalClientScope{
|
||||||
|
SecretPath: infisicalSpec.SecretsScope.SecretsPath,
|
||||||
|
ProjectSlug: infisicalSpec.SecretsScope.ProjectSlug,
|
||||||
|
EnvironmentSlug: infisicalSpec.SecretsScope.EnvironmentSlug,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Provider{}, errors.New("authentication method not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) Close(ctx context.Context) error {
|
||||||
|
if err := p.apiClient.RevokeAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStoreSecretData(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string, secret esmeta.SecretKeySelector) (string, error) {
|
||||||
|
secretRef := esmeta.SecretKeySelector{
|
||||||
|
Name: secret.Name,
|
||||||
|
Key: secret.Key,
|
||||||
|
}
|
||||||
|
if secret.Namespace != nil {
|
||||||
|
secretRef.Namespace = secret.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
secretData, err := resolvers.SecretKeyRef(ctx, kube, store.GetObjectKind().GroupVersionKind().Kind, namespace, &secretRef)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return secretData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
|
||||||
|
storeSpec := store.GetSpec()
|
||||||
|
infisicalStoreSpec := storeSpec.Provider.Infisical
|
||||||
|
if infisicalStoreSpec == nil {
|
||||||
|
return nil, errors.New("invalid infisical store")
|
||||||
|
}
|
||||||
|
|
||||||
|
if infisicalStoreSpec.SecretsScope.EnvironmentSlug == "" || infisicalStoreSpec.SecretsScope.ProjectSlug == "" {
|
||||||
|
return nil, errors.New("secretsScope.projectSlug and secretsScope.environmentSlug cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if infisicalStoreSpec.Auth.UniversalAuthCredentials != nil {
|
||||||
|
uaCredential := infisicalStoreSpec.Auth.UniversalAuthCredentials
|
||||||
|
// to validate reference authentication
|
||||||
|
err := utils.ValidateReferentSecretSelector(store, uaCredential.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = utils.ValidateReferentSecretSelector(store, uaCredential.ClientSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if uaCredential.ClientID.Key == "" || uaCredential.ClientSecret.Key == "" {
|
||||||
|
return nil, errors.New("universalAuthCredentials.clientId and universalAuthCredentials.clientSecret cannot be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
238
pkg/provider/infisical/provider_test.go
Normal file
238
pkg/provider/infisical/provider_test.go
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
/*
|
||||||
|
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 impliec.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package infisical
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
|
esv1meta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
|
||||||
|
"github.com/external-secrets/external-secrets/pkg/provider/infisical/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore
|
||||||
|
|
||||||
|
var apiScope = InfisicalClientScope{
|
||||||
|
SecretPath: "/",
|
||||||
|
ProjectSlug: "first-project",
|
||||||
|
EnvironmentSlug: "dev",
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestCases struct {
|
||||||
|
Name string
|
||||||
|
MockClient *fake.MockInfisicalClient
|
||||||
|
PropertyAccess string
|
||||||
|
Error error
|
||||||
|
Output any
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSecret(t *testing.T) {
|
||||||
|
testCases := []TestCases{
|
||||||
|
{
|
||||||
|
Name: "Get_valid_key",
|
||||||
|
MockClient: &fake.MockInfisicalClient{
|
||||||
|
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
|
||||||
|
return "value", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Error: nil,
|
||||||
|
Output: []byte("value"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Get_property_key",
|
||||||
|
MockClient: &fake.MockInfisicalClient{
|
||||||
|
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
|
||||||
|
return `{"key":"value"}`, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Error: nil,
|
||||||
|
Output: []byte("value"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Key_not_found",
|
||||||
|
MockClient: &fake.MockInfisicalClient{
|
||||||
|
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
|
||||||
|
// from server
|
||||||
|
return "", errors.New("Secret not found")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Error: errors.New("Secret not found"),
|
||||||
|
Output: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
p := &Provider{
|
||||||
|
apiClient: tc.MockClient,
|
||||||
|
apiScope: &apiScope,
|
||||||
|
}
|
||||||
|
var property string
|
||||||
|
if tc.Name == "Get_property_key" {
|
||||||
|
property = "key"
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := p.GetSecret(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
|
||||||
|
Key: "key",
|
||||||
|
Property: property,
|
||||||
|
})
|
||||||
|
|
||||||
|
if tc.Error == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.Output, output)
|
||||||
|
} else {
|
||||||
|
assert.ErrorAs(t, err, &tc.Error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSecretMap(t *testing.T) {
|
||||||
|
testCases := []TestCases{
|
||||||
|
{
|
||||||
|
Name: "Get_valid_key_map",
|
||||||
|
MockClient: &fake.MockInfisicalClient{
|
||||||
|
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
|
||||||
|
return `{"key":"value"}`, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Error: nil,
|
||||||
|
Output: map[string][]byte{
|
||||||
|
"key": []byte("value"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Get_invalid_map",
|
||||||
|
MockClient: &fake.MockInfisicalClient{
|
||||||
|
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
|
||||||
|
return ``, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Error: errors.New("unexpected end of JSON input"),
|
||||||
|
Output: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
p := &Provider{
|
||||||
|
apiClient: tc.MockClient,
|
||||||
|
apiScope: &apiScope,
|
||||||
|
}
|
||||||
|
output, err := p.GetSecretMap(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
|
||||||
|
Key: "key",
|
||||||
|
})
|
||||||
|
if tc.Error == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.Output, output)
|
||||||
|
} else {
|
||||||
|
assert.ErrorAs(t, err, &tc.Error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSecretStore(projectSlug, environment, secretPath string, fn ...storeModifier) *esv1beta1.SecretStore {
|
||||||
|
store := &esv1beta1.SecretStore{
|
||||||
|
Spec: esv1beta1.SecretStoreSpec{
|
||||||
|
Provider: &esv1beta1.SecretStoreProvider{
|
||||||
|
Infisical: &esv1beta1.InfisicalProvider{
|
||||||
|
Auth: esv1beta1.InfisicalAuth{
|
||||||
|
UniversalAuthCredentials: &esv1beta1.UniversalAuthCredentials{},
|
||||||
|
},
|
||||||
|
SecretsScope: esv1beta1.MachineIdentityScopeInWorkspace{
|
||||||
|
SecretsPath: secretPath,
|
||||||
|
EnvironmentSlug: environment,
|
||||||
|
ProjectSlug: projectSlug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, f := range fn {
|
||||||
|
store = f(store)
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func withClientID(name, key string, namespace *string) storeModifier {
|
||||||
|
return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
|
||||||
|
store.Spec.Provider.Infisical.Auth.UniversalAuthCredentials.ClientID = esv1meta.SecretKeySelector{
|
||||||
|
Name: name,
|
||||||
|
Key: key,
|
||||||
|
Namespace: namespace,
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withClientSecret(name, key string, namespace *string) storeModifier {
|
||||||
|
return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
|
||||||
|
store.Spec.Provider.Infisical.Auth.UniversalAuthCredentials.ClientSecret = esv1meta.SecretKeySelector{
|
||||||
|
Name: name,
|
||||||
|
Key: key,
|
||||||
|
Namespace: namespace,
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidateStoreTestCase struct {
|
||||||
|
store *esv1beta1.SecretStore
|
||||||
|
assertError func(t *testing.T, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateStore(t *testing.T) {
|
||||||
|
const randomID = "some-random-id"
|
||||||
|
const authType = "universal-auth"
|
||||||
|
var authCredMissingErr = errors.New("universalAuthCredentials.clientId and universalAuthCredentials.clientSecret cannot be empty")
|
||||||
|
var authScopeMissingErr = errors.New("secretsScope.projectSlug and secretsScope.environmentSlug cannot be empty")
|
||||||
|
|
||||||
|
testCases := []ValidateStoreTestCase{
|
||||||
|
{
|
||||||
|
store: makeSecretStore("", "", ""),
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
require.ErrorAs(t, err, &authScopeMissingErr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientID(authType, randomID, nil)),
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
require.ErrorAs(t, err, &authCredMissingErr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientSecret(authType, randomID, nil)),
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
require.ErrorAs(t, err, &authCredMissingErr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientID(authType, randomID, nil), withClientSecret(authType, randomID, nil)),
|
||||||
|
assertError: func(t *testing.T, err error) { require.NoError(t, err) },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p := Provider{}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
_, err := p.ValidateStore(tc.store)
|
||||||
|
tc.assertError(t, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import (
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
|
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
|
_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
|
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
|
||||||
|
_ "github.com/external-secrets/external-secrets/pkg/provider/infisical"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity"
|
_ "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
|
_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
|
||||||
_ "github.com/external-secrets/external-secrets/pkg/provider/onboardbase"
|
_ "github.com/external-secrets/external-secrets/pkg/provider/onboardbase"
|
||||||
|
|
Loading…
Reference in a new issue