Merge pull request #321 from turkenh/ess-foundation
Add connection package for External Secret Store support
This commit is contained in:
commit
3232ffa5ef
10
Makefile
10
Makefile
|
|
@ -57,16 +57,6 @@ cobertura:
|
|||
grep -v zz_generated.deepcopy | \
|
||||
$(GOCOVER_COBERTURA) > $(GO_TEST_OUTPUT)/cobertura-coverage.xml
|
||||
|
||||
# Ensure a PR is ready for review.
|
||||
reviewable: generate lint
|
||||
@go mod tidy
|
||||
|
||||
# Ensure branch is clean.
|
||||
check-diff: reviewable
|
||||
@$(INFO) checking that branch is clean
|
||||
@git diff --quiet || $(FAIL)
|
||||
@$(OK) branch is clean
|
||||
|
||||
# Update the submodules, such as the common build scripts.
|
||||
submodules:
|
||||
@git submodule sync
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright 2019 The Crossplane Authors.
|
||||
|
||||
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 v1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// PublishConnectionDetailsTo represents configuration of a connection secret.
|
||||
type PublishConnectionDetailsTo struct {
|
||||
// Name is the name of the connection secret.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Metadata is the metadata for connection secret.
|
||||
// +optional
|
||||
Metadata *ConnectionSecretMetadata `json:"metadata,omitempty"`
|
||||
|
||||
// SecretStoreConfigRef specifies which secret store config should be used
|
||||
// for this ConnectionSecret.
|
||||
// +optional
|
||||
// +kubebuilder:default={"name": "default"}
|
||||
SecretStoreConfigRef *Reference `json:"configRef,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectionSecretMetadata represents metadata of a connection secret.
|
||||
type ConnectionSecretMetadata struct {
|
||||
// Labels are the labels/tags to be added to connection secret.
|
||||
// - For Kubernetes secrets, this will be used as "metadata.labels".
|
||||
// - It is up to Secret Store implementation for others store types.
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Annotations are the annotations to be added to connection secret.
|
||||
// - For Kubernetes secrets, this will be used as "metadata.annotations".
|
||||
// - It is up to Secret Store implementation for others store types.
|
||||
// +optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// Type is the SecretType for the connection secret.
|
||||
// - Only valid for Kubernetes Secret Stores.
|
||||
// +optional
|
||||
Type *corev1.SecretType `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// SecretStoreType represents a secret store type.
|
||||
type SecretStoreType string
|
||||
|
||||
const (
|
||||
// SecretStoreKubernetes indicates that secret store type is
|
||||
// Kubernetes. In other words, connection secrets will be stored as K8s
|
||||
// Secrets.
|
||||
SecretStoreKubernetes SecretStoreType = "Kubernetes"
|
||||
|
||||
// SecretStoreVault indicates that secret store type is Vault.
|
||||
SecretStoreVault SecretStoreType = "Vault"
|
||||
)
|
||||
|
||||
// SecretStoreConfig represents configuration of a Secret Store.
|
||||
type SecretStoreConfig struct {
|
||||
// Type configures which secret store to be used. Only the configuration
|
||||
// block for this store will be used and others will be ignored if provided.
|
||||
// Default is Kubernetes.
|
||||
// +optional
|
||||
// +kubebuilder:default=Kubernetes
|
||||
Type *SecretStoreType `json:"type,omitempty"`
|
||||
|
||||
// DefaultScope used for scoping secrets for "cluster-scoped" resources.
|
||||
// If store type is "Kubernetes", this would mean the default namespace to
|
||||
// store connection secrets for cluster scoped resources.
|
||||
// In case of "Vault", this would be used as the default parent path.
|
||||
// Typically, should be set as Crossplane installation namespace.
|
||||
DefaultScope string `json:"defaultScope"`
|
||||
|
||||
// Kubernetes configures a Kubernetes secret store.
|
||||
// If the "type" is "Kubernetes" but no config provided, in cluster config
|
||||
// will be used.
|
||||
// +optional
|
||||
Kubernetes *KubernetesSecretStoreConfig `json:"kubernetes,omitempty"`
|
||||
|
||||
// Vault configures a Vault secret store.
|
||||
// +optional
|
||||
Vault *VaultSecretStoreConfig `json:"vault,omitempty"`
|
||||
}
|
||||
|
||||
// KubernetesAuthConfig required to authenticate to a K8s API. It expects
|
||||
// a "kubeconfig" file to be provided.
|
||||
type KubernetesAuthConfig struct {
|
||||
// Source of the credentials.
|
||||
// +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem
|
||||
Source CredentialsSource `json:"source"`
|
||||
|
||||
// CommonCredentialSelectors provides common selectors for extracting
|
||||
// credentials.
|
||||
CommonCredentialSelectors `json:",inline"`
|
||||
}
|
||||
|
||||
// KubernetesSecretStoreConfig represents the required configuration
|
||||
// for a Kubernetes secret store.
|
||||
type KubernetesSecretStoreConfig struct {
|
||||
// Credentials used to connect to the Kubernetes API.
|
||||
Auth KubernetesAuthConfig `json:"auth"`
|
||||
|
||||
// TODO(turkenh): Support additional identities like
|
||||
// https://github.com/crossplane-contrib/provider-kubernetes/blob/4d722ef914e6964e80e190317daca9872ae98738/apis/v1alpha1/types.go#L34
|
||||
}
|
||||
|
||||
// VaultAuthMethod represent a Vault authentication method.
|
||||
// https://www.vaultproject.io/docs/auth
|
||||
type VaultAuthMethod string
|
||||
|
||||
const (
|
||||
// VaultAuthKubernetes indicates that "Kubernetes Auth" will be used to
|
||||
// authenticate to Vault.
|
||||
// https://www.vaultproject.io/docs/auth/kubernetes
|
||||
VaultAuthKubernetes VaultAuthMethod = "Kubernetes"
|
||||
|
||||
// VaultAuthToken indicates that "Token Auth" will be used to
|
||||
// authenticate to Vault.
|
||||
// https://www.vaultproject.io/docs/auth/token
|
||||
VaultAuthToken VaultAuthMethod = "Token"
|
||||
)
|
||||
|
||||
// VaultAuthKubernetesConfig represents configuration for Vault Kubernetes Auth
|
||||
// Method.
|
||||
// https://www.vaultproject.io/docs/auth
|
||||
type VaultAuthKubernetesConfig struct {
|
||||
// MountPath configures path where the Kubernetes authentication backend is
|
||||
// mounted in Vault.
|
||||
MountPath string `json:"mountPath"`
|
||||
|
||||
// Role configures the Vault Role to assume.
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// VaultAuthConfig required to authenticate to a Vault API.
|
||||
type VaultAuthConfig struct {
|
||||
// Method configures which auth method will be used.
|
||||
Method VaultAuthMethod `json:"method"`
|
||||
// Kubernetes configures Kubernetes Auth for Vault.
|
||||
// +optional
|
||||
Kubernetes *VaultAuthKubernetesConfig `json:"kubernetes,omitempty"`
|
||||
}
|
||||
|
||||
// VaultSecretStoreConfig represents the required configuration for a Vault
|
||||
// secret store.
|
||||
type VaultSecretStoreConfig struct {
|
||||
// Server is the url of the Vault server, e.g. "https://vault.acme.org"
|
||||
Server string `json:"server"`
|
||||
|
||||
// ParentPath is the path to be prepended to all secrets.
|
||||
ParentPath string `json:"parentPath"`
|
||||
|
||||
// Version of the KV Secrets engine of Vault.
|
||||
// https://www.vaultproject.io/docs/secrets/kv
|
||||
// +optional
|
||||
// +kubebuilder:default=v2
|
||||
Version *string `json:"version,omitempty"`
|
||||
|
||||
// CABundle is base64 encoded string of Vaults CA certificate.
|
||||
// +optional
|
||||
CABundle *string `json:"caBundle,omitempty"`
|
||||
|
||||
// CABundleSecretRef is a reference to a K8s secret key with Vaults CA
|
||||
// certificate.
|
||||
// +optional
|
||||
CABundleSecretRef *SecretKeySelector `json:"caBundleSecretRef,omitempty"`
|
||||
|
||||
// Auth configures an authentication method for Vault.
|
||||
Auth VaultAuthConfig `json:"auth"`
|
||||
}
|
||||
|
|
@ -93,6 +93,40 @@ func (in *ConditionedStatus) DeepCopy() *ConditionedStatus {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConnectionSecretMetadata) DeepCopyInto(out *ConnectionSecretMetadata) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Type != nil {
|
||||
in, out := &in.Type, &out.Type
|
||||
*out = new(corev1.SecretType)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSecretMetadata.
|
||||
func (in *ConnectionSecretMetadata) DeepCopy() *ConnectionSecretMetadata {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConnectionSecretMetadata)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EnvSelector) DeepCopyInto(out *EnvSelector) {
|
||||
*out = *in
|
||||
|
|
@ -123,6 +157,38 @@ func (in *FsSelector) DeepCopy() *FsSelector {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubernetesAuthConfig) DeepCopyInto(out *KubernetesAuthConfig) {
|
||||
*out = *in
|
||||
in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesAuthConfig.
|
||||
func (in *KubernetesAuthConfig) DeepCopy() *KubernetesAuthConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KubernetesAuthConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubernetesSecretStoreConfig) DeepCopyInto(out *KubernetesSecretStoreConfig) {
|
||||
*out = *in
|
||||
in.Auth.DeepCopyInto(&out.Auth)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesSecretStoreConfig.
|
||||
func (in *KubernetesSecretStoreConfig) DeepCopy() *KubernetesSecretStoreConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KubernetesSecretStoreConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) {
|
||||
*out = *in
|
||||
|
|
@ -196,6 +262,31 @@ func (in *ProviderConfigUsage) DeepCopy() *ProviderConfigUsage {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PublishConnectionDetailsTo) DeepCopyInto(out *PublishConnectionDetailsTo) {
|
||||
*out = *in
|
||||
if in.Metadata != nil {
|
||||
in, out := &in.Metadata, &out.Metadata
|
||||
*out = new(ConnectionSecretMetadata)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.SecretStoreConfigRef != nil {
|
||||
in, out := &in.SecretStoreConfigRef, &out.SecretStoreConfigRef
|
||||
*out = new(Reference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublishConnectionDetailsTo.
|
||||
func (in *PublishConnectionDetailsTo) DeepCopy() *PublishConnectionDetailsTo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PublishConnectionDetailsTo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Reference) DeepCopyInto(out *Reference) {
|
||||
*out = *in
|
||||
|
|
@ -288,6 +379,36 @@ func (in *SecretReference) DeepCopy() *SecretReference {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretStoreConfig) DeepCopyInto(out *SecretStoreConfig) {
|
||||
*out = *in
|
||||
if in.Type != nil {
|
||||
in, out := &in.Type, &out.Type
|
||||
*out = new(SecretStoreType)
|
||||
**out = **in
|
||||
}
|
||||
if in.Kubernetes != nil {
|
||||
in, out := &in.Kubernetes, &out.Kubernetes
|
||||
*out = new(KubernetesSecretStoreConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Vault != nil {
|
||||
in, out := &in.Vault, &out.Vault
|
||||
*out = new(VaultSecretStoreConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreConfig.
|
||||
func (in *SecretStoreConfig) DeepCopy() *SecretStoreConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretStoreConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Selector) DeepCopyInto(out *Selector) {
|
||||
*out = *in
|
||||
|
|
@ -370,3 +491,69 @@ func (in *TypedReference) DeepCopy() *TypedReference {
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VaultAuthConfig) DeepCopyInto(out *VaultAuthConfig) {
|
||||
*out = *in
|
||||
if in.Kubernetes != nil {
|
||||
in, out := &in.Kubernetes, &out.Kubernetes
|
||||
*out = new(VaultAuthKubernetesConfig)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAuthConfig.
|
||||
func (in *VaultAuthConfig) DeepCopy() *VaultAuthConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VaultAuthConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VaultAuthKubernetesConfig) DeepCopyInto(out *VaultAuthKubernetesConfig) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAuthKubernetesConfig.
|
||||
func (in *VaultAuthKubernetesConfig) DeepCopy() *VaultAuthKubernetesConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VaultAuthKubernetesConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VaultSecretStoreConfig) DeepCopyInto(out *VaultSecretStoreConfig) {
|
||||
*out = *in
|
||||
if in.Version != nil {
|
||||
in, out := &in.Version, &out.Version
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.CABundle != nil {
|
||||
in, out := &in.CABundle, &out.CABundle
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.CABundleSecretRef != nil {
|
||||
in, out := &in.CABundleSecretRef, &out.CABundleSecretRef
|
||||
*out = new(SecretKeySelector)
|
||||
**out = **in
|
||||
}
|
||||
in.Auth.DeepCopyInto(&out.Auth)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretStoreConfig.
|
||||
func (in *VaultSecretStoreConfig) DeepCopy() *VaultSecretStoreConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VaultSecretStoreConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
|
||||
)
|
||||
|
||||
// MockSecretOwner is a mock object that satisfies ConnectionSecretOwner
|
||||
// interface.
|
||||
type MockSecretOwner struct {
|
||||
runtime.Object
|
||||
metav1.ObjectMeta
|
||||
|
||||
To *v1.PublishConnectionDetailsTo
|
||||
}
|
||||
|
||||
// GetPublishConnectionDetailsTo returns the publish connection details to reference.
|
||||
func (m *MockSecretOwner) GetPublishConnectionDetailsTo() *v1.PublishConnectionDetailsTo {
|
||||
return m.To
|
||||
}
|
||||
|
||||
// SetPublishConnectionDetailsTo sets the publish connection details to reference.
|
||||
func (m *MockSecretOwner) SetPublishConnectionDetailsTo(t *v1.PublishConnectionDetailsTo) {
|
||||
m.To = t
|
||||
}
|
||||
|
||||
// GetObjectKind returns schema.ObjectKind.
|
||||
func (m *MockSecretOwner) GetObjectKind() schema.ObjectKind {
|
||||
return schema.EmptyObjectKind
|
||||
}
|
||||
|
||||
// DeepCopyObject returns a copy of the object as runtime.Object
|
||||
func (m *MockSecretOwner) DeepCopyObject() runtime.Object {
|
||||
out := &MockSecretOwner{}
|
||||
j, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = json.Unmarshal(j, out)
|
||||
return out
|
||||
}
|
||||
|
||||
// SecretStore is a fake SecretStore
|
||||
type SecretStore struct {
|
||||
ReadKeyValuesFn func(ctx context.Context, i store.Secret) (store.KeyValues, error)
|
||||
WriteKeyValuesFn func(ctx context.Context, i store.Secret, kv store.KeyValues) error
|
||||
DeleteKeyValuesFn func(ctx context.Context, i store.Secret, kv store.KeyValues) error
|
||||
}
|
||||
|
||||
// ReadKeyValues reads key values.
|
||||
func (ss *SecretStore) ReadKeyValues(ctx context.Context, i store.Secret) (store.KeyValues, error) {
|
||||
return ss.ReadKeyValuesFn(ctx, i)
|
||||
}
|
||||
|
||||
// WriteKeyValues writes key values.
|
||||
func (ss *SecretStore) WriteKeyValues(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
return ss.WriteKeyValuesFn(ctx, i, kv)
|
||||
}
|
||||
|
||||
// DeleteKeyValues deletes key values.
|
||||
func (ss *SecretStore) DeleteKeyValues(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
return ss.DeleteKeyValuesFn(ctx, i, kv)
|
||||
}
|
||||
|
||||
// StoreConfig is a mock implementation of the StoreConfig interface.
|
||||
type StoreConfig struct {
|
||||
metav1.ObjectMeta
|
||||
|
||||
Config v1.SecretStoreConfig
|
||||
v1.ConditionedStatus
|
||||
}
|
||||
|
||||
// GetStoreConfig returns SecretStoreConfig
|
||||
func (s *StoreConfig) GetStoreConfig() v1.SecretStoreConfig {
|
||||
return s.Config
|
||||
}
|
||||
|
||||
// GetObjectKind returns schema.ObjectKind.
|
||||
func (s *StoreConfig) GetObjectKind() schema.ObjectKind {
|
||||
return schema.EmptyObjectKind
|
||||
}
|
||||
|
||||
// DeepCopyObject returns a copy of the object as runtime.Object
|
||||
func (s *StoreConfig) DeepCopyObject() runtime.Object {
|
||||
out := &StoreConfig{}
|
||||
j, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = json.Unmarshal(j, out)
|
||||
return out
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// A DetailsPublisherTo may write a connection details secret to a secret store
|
||||
type DetailsPublisherTo interface {
|
||||
SetPublishConnectionDetailsTo(r *v1.PublishConnectionDetailsTo)
|
||||
GetPublishConnectionDetailsTo() *v1.PublishConnectionDetailsTo
|
||||
}
|
||||
|
||||
// A SecretOwner is a Kubernetes object that owns a connection secret.
|
||||
type SecretOwner interface {
|
||||
resource.Object
|
||||
|
||||
DetailsPublisherTo
|
||||
}
|
||||
|
||||
// A StoreConfig configures a connection store.
|
||||
type StoreConfig interface {
|
||||
resource.Object
|
||||
|
||||
resource.Conditioned
|
||||
GetStoreConfig() v1.SecretStoreConfig
|
||||
}
|
||||
|
||||
// A Store stores sensitive key values in Secret.
|
||||
type Store interface {
|
||||
ReadKeyValues(ctx context.Context, i store.Secret) (store.KeyValues, error)
|
||||
WriteKeyValues(ctx context.Context, i store.Secret, kv store.KeyValues) error
|
||||
DeleteKeyValues(ctx context.Context, i store.Secret, kv store.KeyValues) error
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/logging"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// Error strings.
|
||||
const (
|
||||
errConnectStore = "cannot connect to secret store"
|
||||
errWriteStore = "cannot write to secret store"
|
||||
errDeleteFromStore = "cannot delete from secret store"
|
||||
errGetStoreConfig = "cannot get store config"
|
||||
)
|
||||
|
||||
// StoreBuilderFn is a function that builds and returns a Store with a given
|
||||
// store config.
|
||||
type StoreBuilderFn func(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (Store, error)
|
||||
|
||||
// A DetailsManagerOption configures a DetailsManager.
|
||||
type DetailsManagerOption func(*DetailsManager)
|
||||
|
||||
// WithLogger specifies how the DetailsManager should log messages.
|
||||
func WithLogger(l logging.Logger) DetailsManagerOption {
|
||||
return func(m *DetailsManager) {
|
||||
m.log = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithStoreBuilder configures the StoreBuilder to use.
|
||||
func WithStoreBuilder(sb StoreBuilderFn) DetailsManagerOption {
|
||||
return func(m *DetailsManager) {
|
||||
m.storeBuilder = sb
|
||||
}
|
||||
}
|
||||
|
||||
// DetailsManager is a connection details manager that satisfies the required
|
||||
// interfaces to work with connection details by managing interaction with
|
||||
// different store implementations.
|
||||
type DetailsManager struct {
|
||||
client client.Client
|
||||
newConfig func() StoreConfig
|
||||
storeBuilder StoreBuilderFn
|
||||
|
||||
log logging.Logger
|
||||
}
|
||||
|
||||
// NewDetailsManager returns a new connection DetailsManager.
|
||||
func NewDetailsManager(c client.Client, of schema.GroupVersionKind, o ...DetailsManagerOption) *DetailsManager {
|
||||
nc := func() StoreConfig {
|
||||
return resource.MustCreateObject(of, c.Scheme()).(StoreConfig)
|
||||
}
|
||||
|
||||
// Panic early if we've been asked to reconcile a resource kind that has not
|
||||
// been registered with our controller manager's scheme.
|
||||
_ = nc()
|
||||
|
||||
m := &DetailsManager{
|
||||
client: c,
|
||||
newConfig: nc,
|
||||
storeBuilder: RuntimeStoreBuilder,
|
||||
|
||||
log: logging.NewNopLogger(),
|
||||
}
|
||||
|
||||
for _, mo := range o {
|
||||
mo(m)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// PublishConnection publishes the supplied ConnectionDetails to a secret on
|
||||
// the configured connection Store.
|
||||
// TODO(turkenh): Refactor this method once the `managed.ConnectionPublisher`
|
||||
// interface methods refactored per new types: SecretOwner and KeyValues
|
||||
func (m *DetailsManager) PublishConnection(ctx context.Context, mg resource.Managed, c managed.ConnectionDetails) error {
|
||||
return m.publishConnection(ctx, mg.(SecretOwner), store.KeyValues(c))
|
||||
}
|
||||
|
||||
// UnpublishConnection deletes connection details secret from the configured
|
||||
// connection Store.
|
||||
// TODO(turkenh): Refactor this method once the `managed.ConnectionPublisher`
|
||||
// interface methods refactored per new types: SecretOwner and KeyValues
|
||||
func (m *DetailsManager) UnpublishConnection(ctx context.Context, mg resource.Managed, c managed.ConnectionDetails) error {
|
||||
return m.unpublishConnection(ctx, mg.(SecretOwner), store.KeyValues(c))
|
||||
}
|
||||
|
||||
func (m *DetailsManager) connectStore(ctx context.Context, p *v1.PublishConnectionDetailsTo) (Store, error) {
|
||||
sc := m.newConfig()
|
||||
if err := m.client.Get(ctx, types.NamespacedName{Name: p.SecretStoreConfigRef.Name}, sc); err != nil {
|
||||
return nil, errors.Wrap(err, errGetStoreConfig)
|
||||
}
|
||||
|
||||
return m.storeBuilder(ctx, m.client, sc.GetStoreConfig())
|
||||
}
|
||||
|
||||
func (m *DetailsManager) publishConnection(ctx context.Context, so SecretOwner, kv store.KeyValues) error {
|
||||
// This resource does not want to expose a connection secret.
|
||||
p := so.GetPublishConnectionDetailsTo()
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ss, err := m.connectStore(ctx, p)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errConnectStore)
|
||||
}
|
||||
|
||||
return errors.Wrap(ss.WriteKeyValues(ctx, store.Secret{
|
||||
Name: p.Name,
|
||||
Scope: so.GetNamespace(),
|
||||
Metadata: p.Metadata,
|
||||
}, kv), errWriteStore)
|
||||
}
|
||||
|
||||
func (m *DetailsManager) unpublishConnection(ctx context.Context, so SecretOwner, kv store.KeyValues) error {
|
||||
// This resource didn't expose a connection secret.
|
||||
p := so.GetPublishConnectionDetailsTo()
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ss, err := m.connectStore(ctx, p)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errConnectStore)
|
||||
}
|
||||
|
||||
return errors.Wrap(ss.DeleteKeyValues(ctx, store.Secret{
|
||||
Name: p.Name,
|
||||
Scope: so.GetNamespace(),
|
||||
Metadata: p.Metadata,
|
||||
}, kv), errDeleteFromStore)
|
||||
}
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/fake"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
||||
resourcefake "github.com/crossplane/crossplane-runtime/pkg/resource/fake"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
)
|
||||
|
||||
const (
|
||||
SecretStoreFake v1.SecretStoreType = "Fake"
|
||||
|
||||
fakeConfig = "fake"
|
||||
)
|
||||
|
||||
const (
|
||||
errBuildStore = "cannot build store"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeStore = SecretStoreFake
|
||||
)
|
||||
|
||||
func TestManagerConnectStore(t *testing.T) {
|
||||
type args struct {
|
||||
c client.Client
|
||||
sb StoreBuilderFn
|
||||
|
||||
p *v1.PublishConnectionDetailsTo
|
||||
}
|
||||
|
||||
type want struct {
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args
|
||||
want
|
||||
}{
|
||||
"ConfigNotFound": {
|
||||
reason: "We should return a proper error if referenced StoreConfig does not exist.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
return kerrors.NewNotFound(schema.GroupResource{}, key.Name)
|
||||
},
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
sb: fakeStoreBuilderFn(fake.SecretStore{}),
|
||||
p: &v1.PublishConnectionDetailsTo{
|
||||
SecretStoreConfigRef: &v1.Reference{
|
||||
Name: fakeConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, fakeConfig), errGetStoreConfig),
|
||||
},
|
||||
},
|
||||
"BuildStoreError": {
|
||||
reason: "We should return any error encountered while building the Store.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
*obj.(*fake.StoreConfig) = fake.StoreConfig{}
|
||||
return nil
|
||||
},
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
sb: func(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (Store, error) {
|
||||
return nil, errors.New(errBuildStore)
|
||||
},
|
||||
p: &v1.PublishConnectionDetailsTo{
|
||||
SecretStoreConfigRef: &v1.Reference{
|
||||
Name: fakeConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.New(errBuildStore),
|
||||
},
|
||||
},
|
||||
"SuccessfulConnect": {
|
||||
reason: "We should not return an error when connected successfully.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
*obj.(*fake.StoreConfig) = fake.StoreConfig{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fakeConfig,
|
||||
},
|
||||
Config: v1.SecretStoreConfig{
|
||||
Type: &fakeStore,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
sb: fakeStoreBuilderFn(fake.SecretStore{}),
|
||||
p: &v1.PublishConnectionDetailsTo{
|
||||
SecretStoreConfigRef: &v1.Reference{
|
||||
Name: fakeConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb))
|
||||
|
||||
_, err := m.connectStore(context.Background(), tc.args.p)
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\nReason: %s\nm.connectStore(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerPublishConnection(t *testing.T) {
|
||||
type args struct {
|
||||
c client.Client
|
||||
sb StoreBuilderFn
|
||||
|
||||
kv store.KeyValues
|
||||
so SecretOwner
|
||||
}
|
||||
|
||||
type want struct {
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args
|
||||
want
|
||||
}{
|
||||
"NoConnectionDetails": {
|
||||
reason: "We should return no error if resource does not want to expose a connection secret.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
so: &fake.MockSecretOwner{To: nil},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"CannotConnect": {
|
||||
reason: "We should return any error encountered while connecting to Store.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
return kerrors.NewNotFound(schema.GroupResource{}, key.Name)
|
||||
},
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
sb: fakeStoreBuilderFn(fake.SecretStore{
|
||||
WriteKeyValuesFn: func(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
so: &fake.MockSecretOwner{
|
||||
To: &v1.PublishConnectionDetailsTo{
|
||||
SecretStoreConfigRef: &v1.Reference{
|
||||
Name: "non-existing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, "non-existing"), errGetStoreConfig), errConnectStore),
|
||||
},
|
||||
},
|
||||
"SuccessfulPublish": {
|
||||
reason: "We should return no error when published successfully.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
*obj.(*fake.StoreConfig) = fake.StoreConfig{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fakeConfig,
|
||||
},
|
||||
Config: v1.SecretStoreConfig{
|
||||
Type: &fakeStore,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
sb: fakeStoreBuilderFn(fake.SecretStore{
|
||||
WriteKeyValuesFn: func(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
so: &fake.MockSecretOwner{
|
||||
To: &v1.PublishConnectionDetailsTo{
|
||||
SecretStoreConfigRef: &v1.Reference{
|
||||
Name: "fake",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb))
|
||||
|
||||
err := m.publishConnection(context.Background(), tc.args.so, tc.args.kv)
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\nReason: %s\nm.publishConnection(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerUnpublishConnection(t *testing.T) {
|
||||
type args struct {
|
||||
c client.Client
|
||||
sb StoreBuilderFn
|
||||
|
||||
kv store.KeyValues
|
||||
so SecretOwner
|
||||
}
|
||||
|
||||
type want struct {
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args
|
||||
want
|
||||
}{
|
||||
"NoConnectionDetails": {
|
||||
reason: "We should return no error if resource does not want to expose a connection secret.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
so: &fake.MockSecretOwner{To: nil},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"CannotConnect": {
|
||||
reason: "We should return any error encountered while connecting to Store.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
return kerrors.NewNotFound(schema.GroupResource{}, key.Name)
|
||||
},
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
sb: fakeStoreBuilderFn(fake.SecretStore{
|
||||
WriteKeyValuesFn: func(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
so: &fake.MockSecretOwner{
|
||||
To: &v1.PublishConnectionDetailsTo{
|
||||
SecretStoreConfigRef: &v1.Reference{
|
||||
Name: "non-existing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errors.Wrapf(kerrors.NewNotFound(schema.GroupResource{}, "non-existing"), errGetStoreConfig), errConnectStore),
|
||||
},
|
||||
},
|
||||
"SuccessfulUnpublish": {
|
||||
reason: "We should return no error when unpublished successfully.",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
*obj.(*fake.StoreConfig) = fake.StoreConfig{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fakeConfig,
|
||||
},
|
||||
Config: v1.SecretStoreConfig{
|
||||
Type: &fakeStore,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockScheme: test.NewMockSchemeFn(resourcefake.SchemeWith(&fake.StoreConfig{})),
|
||||
},
|
||||
sb: fakeStoreBuilderFn(fake.SecretStore{
|
||||
DeleteKeyValuesFn: func(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
so: &fake.MockSecretOwner{
|
||||
To: &v1.PublishConnectionDetailsTo{
|
||||
SecretStoreConfigRef: &v1.Reference{
|
||||
Name: "fake",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
m := NewDetailsManager(tc.args.c, resourcefake.GVK(&fake.StoreConfig{}), WithStoreBuilder(tc.args.sb))
|
||||
|
||||
err := m.unpublishConnection(context.Background(), tc.args.so, tc.args.kv)
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\nReason: %s\nm.unpublishConnection(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fakeStoreBuilderFn(ss fake.SecretStore) StoreBuilderFn {
|
||||
return func(_ context.Context, _ client.Client, cfg v1.SecretStoreConfig) (Store, error) {
|
||||
if *cfg.Type == fakeStore {
|
||||
return &ss, nil
|
||||
}
|
||||
return nil, errors.Errorf(errFmtUnknownSecretStore, *cfg.Type)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// Error strings.
|
||||
const (
|
||||
errGetSecret = "cannot get secret"
|
||||
errDeleteSecret = "cannot delete secret"
|
||||
errUpdateSecret = "cannot update secret"
|
||||
errApplySecret = "cannot apply secret"
|
||||
|
||||
errExtractKubernetesAuthCreds = "cannot extract kubernetes auth credentials"
|
||||
errBuildRestConfig = "cannot build rest config kubeconfig"
|
||||
errBuildClient = "cannot build Kubernetes client"
|
||||
)
|
||||
|
||||
// SecretStore is a Kubernetes Secret Store.
|
||||
type SecretStore struct {
|
||||
client resource.ClientApplicator
|
||||
|
||||
defaultNamespace string
|
||||
}
|
||||
|
||||
// NewSecretStore returns a new Kubernetes SecretStore.
|
||||
func NewSecretStore(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (*SecretStore, error) {
|
||||
kube, err := buildClient(ctx, local, cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, errBuildClient)
|
||||
}
|
||||
|
||||
return &SecretStore{
|
||||
client: resource.ClientApplicator{
|
||||
Client: kube,
|
||||
Applicator: resource.NewApplicatorWithRetry(resource.NewAPIPatchingApplicator(kube), resource.IsAPIErrorWrapped, nil),
|
||||
},
|
||||
defaultNamespace: cfg.DefaultScope,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildClient(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (client.Client, error) {
|
||||
if cfg.Kubernetes == nil {
|
||||
// No KubernetesSecretStoreConfig provided, local API Server will be
|
||||
// used as Secret Store.
|
||||
return local, nil
|
||||
}
|
||||
// Configure client for an external API server with a given Kubeconfig.
|
||||
kfg, err := resource.CommonCredentialExtractor(ctx, cfg.Kubernetes.Auth.Source, local, cfg.Kubernetes.Auth.CommonCredentialSelectors)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, errExtractKubernetesAuthCreds)
|
||||
}
|
||||
config, err := clientcmd.RESTConfigFromKubeConfig(kfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, errBuildRestConfig)
|
||||
}
|
||||
return client.New(config, client.Options{})
|
||||
}
|
||||
|
||||
// ReadKeyValues reads and returns key value pairs for a given Kubernetes Secret.
|
||||
func (ss *SecretStore) ReadKeyValues(ctx context.Context, i store.Secret) (store.KeyValues, error) {
|
||||
s := &corev1.Secret{}
|
||||
return s.Data, errors.Wrap(ss.client.Get(ctx, types.NamespacedName{Name: i.Name, Namespace: ss.namespaceForSecret(i)}, s), errGetSecret)
|
||||
}
|
||||
|
||||
// WriteKeyValues writes key value pairs to a given Kubernetes Secret.
|
||||
func (ss *SecretStore) WriteKeyValues(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
s := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: i.Name,
|
||||
Namespace: ss.namespaceForSecret(i),
|
||||
},
|
||||
Type: resource.SecretTypeConnection,
|
||||
Data: kv,
|
||||
}
|
||||
|
||||
if i.Metadata != nil {
|
||||
s.Labels = i.Metadata.Labels
|
||||
s.Annotations = i.Metadata.Annotations
|
||||
if i.Metadata.Type != nil {
|
||||
s.Type = *i.Metadata.Type
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Wrap(ss.client.Apply(ctx, s), errApplySecret)
|
||||
}
|
||||
|
||||
// DeleteKeyValues delete key value pairs from a given Kubernetes Secret.
|
||||
// If no kv specified, the whole secret instance is deleted.
|
||||
// If kv specified, those would be deleted and secret instance will be deleted
|
||||
// only if there is no data left.
|
||||
func (ss *SecretStore) DeleteKeyValues(ctx context.Context, i store.Secret, kv store.KeyValues) error {
|
||||
// NOTE(turkenh): DeleteKeyValues method wouldn't need to do anything if we
|
||||
// have used owner references similar to existing implementation. However,
|
||||
// this wouldn't work if the K8s API is not the same as where SecretOwner
|
||||
// object lives, i.e. a remote cluster.
|
||||
// Considering there is not much additional value with deletion via garbage
|
||||
// collection in this specific case other than one less API call during
|
||||
// deletion, I opted for unifying both instead of adding conditional logic
|
||||
// like add owner references if not remote and not call delete etc.
|
||||
s := &corev1.Secret{}
|
||||
err := ss.client.Get(ctx, types.NamespacedName{Name: i.Name, Namespace: ss.namespaceForSecret(i)}, s)
|
||||
if kerrors.IsNotFound(err) {
|
||||
// Secret already deleted, nothing to do.
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errGetSecret)
|
||||
}
|
||||
// Delete all supplied keys from secret data
|
||||
for k := range kv {
|
||||
delete(s.Data, k)
|
||||
}
|
||||
if len(kv) == 0 || len(s.Data) == 0 {
|
||||
// Secret is deleted only if:
|
||||
// - No kv to delete specified as input
|
||||
// - No data left in the secret
|
||||
return errors.Wrapf(ss.client.Delete(ctx, s), errDeleteSecret)
|
||||
}
|
||||
// If there are still keys left, update the secret with the remaining.
|
||||
return errors.Wrapf(ss.client.Update(ctx, s), errUpdateSecret)
|
||||
}
|
||||
|
||||
func (ss *SecretStore) namespaceForSecret(i store.Secret) string {
|
||||
if i.Scope == "" {
|
||||
return ss.defaultNamespace
|
||||
}
|
||||
return i.Scope
|
||||
}
|
||||
|
|
@ -0,0 +1,667 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/store"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
errBoom = errors.New("boom")
|
||||
|
||||
fakeSecretName = "fake"
|
||||
fakeSecretNamespace = "fake-namespace"
|
||||
|
||||
fakeKV = map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
"key3": []byte("value3"),
|
||||
}
|
||||
|
||||
fakeLabels = map[string]string{
|
||||
"environment": "unit-test",
|
||||
"reason": "testing",
|
||||
}
|
||||
|
||||
fakeAnnotations = map[string]string{
|
||||
"some-annotation-key": "some-annotation-value",
|
||||
}
|
||||
|
||||
storeTypeKubernetes = v1.SecretStoreKubernetes
|
||||
)
|
||||
|
||||
func TestSecretStoreReadKeyValues(t *testing.T) {
|
||||
type args struct {
|
||||
client resource.ClientApplicator
|
||||
secret store.Secret
|
||||
}
|
||||
type want struct {
|
||||
result store.KeyValues
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args
|
||||
want
|
||||
}{
|
||||
"CannotGetSecret": {
|
||||
reason: "Should return a proper error if cannot get the secret",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(errBoom),
|
||||
},
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errBoom, errGetSecret),
|
||||
},
|
||||
},
|
||||
"SuccessfulRead": {
|
||||
reason: "Should return all key values after a success read",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
*obj.(*corev1.Secret) = corev1.Secret{
|
||||
Data: fakeKV,
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: "fake",
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
result: store.KeyValues(fakeKV),
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ss := &SecretStore{
|
||||
client: tc.args.client,
|
||||
}
|
||||
|
||||
got, err := ss.ReadKeyValues(context.Background(), tc.args.secret)
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nss.ReadKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.want.result, got); diff != "" {
|
||||
t.Errorf("\n%s\nss.ReadKeyValues(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretStoreWriteKeyValues(t *testing.T) {
|
||||
secretTypeOpaque := corev1.SecretTypeOpaque
|
||||
type args struct {
|
||||
client resource.ClientApplicator
|
||||
defaultNamespace string
|
||||
secret store.Secret
|
||||
kv store.KeyValues
|
||||
}
|
||||
type want struct {
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args
|
||||
want
|
||||
}{
|
||||
"ApplyFailed": {
|
||||
reason: "Should return a proper error when cannot apply.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
|
||||
return errBoom
|
||||
}),
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
kv: store.KeyValues(fakeKV),
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errBoom, errApplySecret),
|
||||
},
|
||||
},
|
||||
"SecretAlreadyUpToDate": {
|
||||
reason: "Should not change secret if already up to date.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
|
||||
if diff := cmp.Diff(fakeConnectionSecret(withData(fakeKV)), obj.(*corev1.Secret)); diff != "" {
|
||||
t.Errorf("r: -want, +got:\n%s", diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
kv: store.KeyValues(fakeKV),
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"SecretUpdatedWithNewValue": {
|
||||
reason: "Should update value for an existing key if changed.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
|
||||
if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{
|
||||
"existing-key": []byte("new-value"),
|
||||
})), obj.(*corev1.Secret)); diff != "" {
|
||||
t.Errorf("r: -want, +got:\n%s", diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
kv: store.KeyValues(map[string][]byte{
|
||||
"existing-key": []byte("new-value"),
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"SecretPatchedWithNewKey": {
|
||||
reason: "Should update existing secret additively if a new key added",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
|
||||
if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{
|
||||
"new-key": []byte("new-value"),
|
||||
})), obj.(*corev1.Secret)); diff != "" {
|
||||
t.Errorf("r: -want, +got:\n%s", diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
kv: store.KeyValues(map[string][]byte{
|
||||
"new-key": []byte("new-value"),
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"SecretCreatedWithData": {
|
||||
reason: "Should create a secret with all key values with default type.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
|
||||
if diff := cmp.Diff(fakeConnectionSecret(withData(fakeKV)), obj.(*corev1.Secret)); diff != "" {
|
||||
t.Errorf("r: -want, +got:\n%s", diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
kv: store.KeyValues(fakeKV),
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"SecretCreatedWithDataAndMetadata": {
|
||||
reason: "Should create a secret with all key values and provided metadata data.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Applicator: resource.ApplyFn(func(ctx context.Context, obj client.Object, option ...resource.ApplyOption) error {
|
||||
if diff := cmp.Diff(fakeConnectionSecret(
|
||||
withData(fakeKV),
|
||||
withType(corev1.SecretTypeOpaque),
|
||||
withLabels(fakeLabels),
|
||||
withAnnotations(fakeAnnotations)), obj.(*corev1.Secret)); diff != "" {
|
||||
t.Errorf("r: -want, +got:\n%s", diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
Metadata: &v1.ConnectionSecretMetadata{
|
||||
Labels: map[string]string{
|
||||
"environment": "unit-test",
|
||||
"reason": "testing",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"some-annotation-key": "some-annotation-value",
|
||||
},
|
||||
Type: &secretTypeOpaque,
|
||||
},
|
||||
},
|
||||
kv: store.KeyValues(fakeKV),
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ss := &SecretStore{
|
||||
client: tc.args.client,
|
||||
defaultNamespace: tc.args.defaultNamespace,
|
||||
}
|
||||
err := ss.WriteKeyValues(context.Background(), tc.args.secret, tc.args.kv)
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nss.WriteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretStoreDeleteKeyValues(t *testing.T) {
|
||||
type args struct {
|
||||
client resource.ClientApplicator
|
||||
defaultNamespace string
|
||||
secret store.Secret
|
||||
kv store.KeyValues
|
||||
}
|
||||
type want struct {
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args
|
||||
want
|
||||
}{
|
||||
"CannotGetSecret": {
|
||||
reason: "Should return a proper error when it fails to get secret.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(errBoom),
|
||||
},
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errBoom, errGetSecret),
|
||||
},
|
||||
},
|
||||
"SecretUpdatedWithRemainingKeys": {
|
||||
reason: "Should remove supplied keys from secret and update with remaining.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
*obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV))
|
||||
return nil
|
||||
}),
|
||||
MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
if diff := cmp.Diff(fakeConnectionSecret(withData(map[string][]byte{"key3": []byte("value3")})), obj.(*corev1.Secret)); diff != "" {
|
||||
t.Errorf("r: -want, +got:\n%s", diff)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
kv: store.KeyValues(map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"CannotDeleteSecret": {
|
||||
reason: "Should return a proper error when it fails to delete secret.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
*obj.(*corev1.Secret) = *fakeConnectionSecret()
|
||||
return nil
|
||||
}),
|
||||
MockDelete: test.NewMockDeleteFn(errBoom),
|
||||
},
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errBoom, errDeleteSecret),
|
||||
},
|
||||
},
|
||||
"SecretAlreadyDeleted": {
|
||||
reason: "Should not return error if secret already deleted.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
return kerrors.NewNotFound(schema.GroupResource{}, "")
|
||||
}),
|
||||
},
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"SecretDeletedNoKVSupplied": {
|
||||
reason: "Should delete the whole secret if no kv supplied as parameter.",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
*obj.(*corev1.Secret) = *fakeConnectionSecret(withData(fakeKV))
|
||||
return nil
|
||||
}),
|
||||
MockDelete: func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
secret: store.Secret{
|
||||
Name: fakeSecretName,
|
||||
Scope: fakeSecretNamespace,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ss := &SecretStore{
|
||||
client: tc.args.client,
|
||||
defaultNamespace: tc.args.defaultNamespace,
|
||||
}
|
||||
err := ss.DeleteKeyValues(context.Background(), tc.args.secret, tc.args.kv)
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nss.DeleteKeyValues(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSecretStore(t *testing.T) {
|
||||
type args struct {
|
||||
client resource.ClientApplicator
|
||||
cfg v1.SecretStoreConfig
|
||||
}
|
||||
type want struct {
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args
|
||||
want
|
||||
}{
|
||||
"SuccessfulLocal": {
|
||||
reason: "Should return no error after successfully building local Kubernetes secret store",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{},
|
||||
cfg: v1.SecretStoreConfig{
|
||||
Type: &storeTypeKubernetes,
|
||||
DefaultScope: "test-ns",
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"NoSecretWithRemoteKubeconfig": {
|
||||
reason: "Should fail properly if configured kubeconfig secret does not exist",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
return kerrors.NewNotFound(schema.GroupResource{}, "kube-conn")
|
||||
}),
|
||||
},
|
||||
},
|
||||
cfg: v1.SecretStoreConfig{
|
||||
Type: &storeTypeKubernetes,
|
||||
DefaultScope: "test-ns",
|
||||
Kubernetes: &v1.KubernetesSecretStoreConfig{
|
||||
Auth: v1.KubernetesAuthConfig{
|
||||
Source: v1.CredentialsSourceSecret,
|
||||
CommonCredentialSelectors: v1.CommonCredentialSelectors{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
SecretReference: v1.SecretReference{
|
||||
Name: "kube-conn",
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
Key: "kubeconfig",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errors.Wrap(errors.Wrap(kerrors.NewNotFound(schema.GroupResource{}, "kube-conn"), "cannot get credentials secret"), errExtractKubernetesAuthCreds), errBuildClient),
|
||||
},
|
||||
},
|
||||
"InvalidRestConfigForRemote": {
|
||||
reason: "Should fetch the configured kubeconfig and fail if it is not valid",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
*obj.(*corev1.Secret) = corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-conn",
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"kubeconfig": []byte(`
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
malformed
|
||||
`),
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
},
|
||||
cfg: v1.SecretStoreConfig{
|
||||
Type: &storeTypeKubernetes,
|
||||
DefaultScope: "test-ns",
|
||||
Kubernetes: &v1.KubernetesSecretStoreConfig{
|
||||
Auth: v1.KubernetesAuthConfig{
|
||||
Source: v1.CredentialsSourceSecret,
|
||||
CommonCredentialSelectors: v1.CommonCredentialSelectors{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
SecretReference: v1.SecretReference{
|
||||
Name: "kube-conn",
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
Key: "kubeconfig",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errors.Wrap(errors.New("yaml: line 5: could not find expected ':'"), errBuildRestConfig), errBuildClient),
|
||||
},
|
||||
},
|
||||
"InvalidKubeconfigForRemote": {
|
||||
reason: "Should fetch the configured kubeconfig and fail if it is not valid",
|
||||
args: args{
|
||||
client: resource.ClientApplicator{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
*obj.(*corev1.Secret) = corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-conn",
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"kubeconfig": []byte(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: TEST
|
||||
server: https://127.0.0.1:64695
|
||||
name: kind-kind
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-kind
|
||||
namespace: crossplane-system
|
||||
user: kind-kind
|
||||
name: kind-kind
|
||||
current-context: kind-kind
|
||||
kind: Config
|
||||
users:
|
||||
- name: kind-kind
|
||||
user: {}
|
||||
`),
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
},
|
||||
cfg: v1.SecretStoreConfig{
|
||||
Type: &storeTypeKubernetes,
|
||||
DefaultScope: "test-ns",
|
||||
Kubernetes: &v1.KubernetesSecretStoreConfig{
|
||||
Auth: v1.KubernetesAuthConfig{
|
||||
Source: v1.CredentialsSourceSecret,
|
||||
CommonCredentialSelectors: v1.CommonCredentialSelectors{
|
||||
SecretRef: &v1.SecretKeySelector{
|
||||
SecretReference: v1.SecretReference{
|
||||
Name: "kube-conn",
|
||||
Namespace: "test-ns",
|
||||
},
|
||||
Key: "kubeconfig",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: errors.Wrap(errors.New("unable to load root certificates: unable to parse bytes as PEM block"), errBuildClient),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := NewSecretStore(context.Background(), tc.args.client, tc.args.cfg)
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nNewSecretStore(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type secretOption func(*corev1.Secret)
|
||||
|
||||
func withType(t corev1.SecretType) secretOption {
|
||||
return func(s *corev1.Secret) {
|
||||
s.Type = t
|
||||
}
|
||||
}
|
||||
|
||||
func withData(d map[string][]byte) secretOption {
|
||||
return func(s *corev1.Secret) {
|
||||
s.Data = d
|
||||
}
|
||||
}
|
||||
|
||||
func withLabels(l map[string]string) secretOption {
|
||||
return func(s *corev1.Secret) {
|
||||
s.Labels = l
|
||||
}
|
||||
}
|
||||
|
||||
func withAnnotations(a map[string]string) secretOption {
|
||||
return func(s *corev1.Secret) {
|
||||
s.Annotations = a
|
||||
}
|
||||
}
|
||||
func fakeConnectionSecret(opts ...secretOption) *corev1.Secret {
|
||||
s := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fakeSecretName,
|
||||
Namespace: fakeSecretNamespace,
|
||||
},
|
||||
Type: resource.SecretTypeConnection,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(s)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 store
|
||||
|
||||
import (
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
)
|
||||
|
||||
// KeyValues is a map with sensitive values.
|
||||
type KeyValues map[string][]byte
|
||||
|
||||
// A Secret is an entity representing a set of sensitive Key Values.
|
||||
type Secret struct {
|
||||
Name string
|
||||
Scope string
|
||||
Metadata *v1.ConnectionSecretMetadata
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2022 The Crossplane Authors.
|
||||
|
||||
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 connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/connection/store/kubernetes"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
errFmtUnknownSecretStore = "unknown secret store type: %q"
|
||||
)
|
||||
|
||||
// RuntimeStoreBuilder builds and returns a Store for any supported Store type
|
||||
// in a given config.
|
||||
//
|
||||
// All in-tree connection Store implementations needs to be registered here.
|
||||
func RuntimeStoreBuilder(ctx context.Context, local client.Client, cfg v1.SecretStoreConfig) (Store, error) {
|
||||
switch *cfg.Type {
|
||||
case v1.SecretStoreKubernetes:
|
||||
return kubernetes.NewSecretStore(ctx, local, cfg)
|
||||
case v1.SecretStoreVault:
|
||||
return nil, errors.New("Vault is not supported as a secret store yet")
|
||||
}
|
||||
return nil, errors.Errorf(errFmtUnknownSecretStore, *cfg.Type)
|
||||
}
|
||||
|
|
@ -53,6 +53,9 @@ type MockStatusUpdateFn func(ctx context.Context, obj client.Object, opts ...cli
|
|||
// A MockStatusPatchFn is used to mock client.Client's StatusUpdate implementation.
|
||||
type MockStatusPatchFn func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error
|
||||
|
||||
// A MockSchemeFn is used to mock client.Client's Scheme implementation.
|
||||
type MockSchemeFn func() *runtime.Scheme
|
||||
|
||||
// An ObjectFn operates on the supplied Object. You might use an ObjectFn to
|
||||
// test or update the contents of an Object.
|
||||
type ObjectFn func(obj client.Object) error
|
||||
|
|
@ -169,6 +172,13 @@ func NewMockStatusPatchFn(err error, ofn ...ObjectFn) MockStatusPatchFn {
|
|||
}
|
||||
}
|
||||
|
||||
// NewMockSchemeFn returns a MockSchemeFn that returns the scheme
|
||||
func NewMockSchemeFn(scheme *runtime.Scheme) MockSchemeFn {
|
||||
return func() *runtime.Scheme {
|
||||
return scheme
|
||||
}
|
||||
}
|
||||
|
||||
// MockClient implements controller-runtime's Client interface, allowing each
|
||||
// method to be overridden for testing. The controller-runtime provides a fake
|
||||
// client, but it is has surprising side effects (e.g. silently calling
|
||||
|
|
@ -183,6 +193,8 @@ type MockClient struct {
|
|||
MockPatch MockPatchFn
|
||||
MockStatusUpdate MockStatusUpdateFn
|
||||
MockStatusPatch MockStatusPatchFn
|
||||
|
||||
MockScheme MockSchemeFn
|
||||
}
|
||||
|
||||
// NewMockClient returns a MockClient that does nothing when its methods are
|
||||
|
|
@ -198,6 +210,8 @@ func NewMockClient() *MockClient {
|
|||
MockPatch: NewMockPatchFn(nil),
|
||||
MockStatusUpdate: NewMockStatusUpdateFn(nil),
|
||||
MockStatusPatch: NewMockStatusPatchFn(nil),
|
||||
|
||||
MockScheme: NewMockSchemeFn(nil),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,9 +263,9 @@ func (c *MockClient) RESTMapper() meta.RESTMapper {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Scheme returns the current runtime scheme.
|
||||
// Scheme calls MockClient's MockScheme function
|
||||
func (c *MockClient) Scheme() *runtime.Scheme {
|
||||
return nil
|
||||
return c.MockScheme()
|
||||
}
|
||||
|
||||
// MockStatusWriter provides mock functionality for status sub-resource
|
||||
|
|
|
|||
Loading…
Reference in New Issue