diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9442f35b..e50ae00a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+### Added
+- The operator can now remove external resources: Vault, Git Repository and Files in a repository
+
## v0.1.5 - 2020-06-12
### Added
- Kustomize setup ([#71])
diff --git a/deploy/crds/syn.tools_clusters_crd.yaml b/deploy/crds/syn.tools_clusters_crd.yaml
index ee2891ec..2bf2e1c6 100644
--- a/deploy/crds/syn.tools_clusters_crd.yaml
+++ b/deploy/crds/syn.tools_clusters_crd.yaml
@@ -41,6 +41,16 @@ spec:
spec:
description: ClusterSpec defines the desired state of Cluster
properties:
+ deletionPolicy:
+ description: 'DeletionPolicy defines how the external resources should
+ be treated upon CR deletion. Retain: will not delete any external
+ resources Delete: will delete the external resources Archive: will
+ archive the external resources, if it supports that'
+ enum:
+ - Delete
+ - Retain
+ - Archive
+ type: string
displayName:
description: DisplayName of cluster which could be different from metadata.name.
Allows cluster renaming should it be needed.
@@ -69,6 +79,16 @@ spec:
name must be unique.
type: string
type: object
+ deletionPolicy:
+ description: 'DeletionPolicy defines how the external resources
+ should be treated upon CR deletion. Retain: will not delete any
+ external resources Delete: will delete the external resources
+ Archive: will archive the external resources, if it supports that'
+ enum:
+ - Delete
+ - Retain
+ - Archive
+ type: string
deployKeys:
additionalProperties:
description: DeployKey defines an SSH key to be used for git operations.
diff --git a/deploy/crds/syn.tools_gitrepos_crd.yaml b/deploy/crds/syn.tools_gitrepos_crd.yaml
index 69dea9db..8ceea1d8 100644
--- a/deploy/crds/syn.tools_gitrepos_crd.yaml
+++ b/deploy/crds/syn.tools_gitrepos_crd.yaml
@@ -57,6 +57,16 @@ spec:
name must be unique.
type: string
type: object
+ deletionPolicy:
+ description: 'DeletionPolicy defines how the external resources should
+ be treated upon CR deletion. Retain: will not delete any external
+ resources Delete: will delete the external resources Archive: will
+ archive the external resources, if it supports that'
+ enum:
+ - Delete
+ - Retain
+ - Archive
+ type: string
deployKeys:
additionalProperties:
description: DeployKey defines an SSH key to be used for git operations.
diff --git a/deploy/crds/syn.tools_tenants_crd.yaml b/deploy/crds/syn.tools_tenants_crd.yaml
index d8572d53..33935c3a 100644
--- a/deploy/crds/syn.tools_tenants_crd.yaml
+++ b/deploy/crds/syn.tools_tenants_crd.yaml
@@ -38,6 +38,16 @@ spec:
spec:
description: TenantSpec defines the desired state of Tenant
properties:
+ deletionPolicy:
+ description: 'DeletionPolicy defines how the external resources should
+ be treated upon CR deletion. Retain: will not delete any external
+ resources Delete: will delete the external resources Archive: will
+ archive the external resources, if it supports that'
+ enum:
+ - Delete
+ - Retain
+ - Archive
+ type: string
displayName:
description: DisplayName is the display name of the tenant.
type: string
@@ -58,6 +68,16 @@ spec:
name must be unique.
type: string
type: object
+ deletionPolicy:
+ description: 'DeletionPolicy defines how the external resources
+ should be treated upon CR deletion. Retain: will not delete any
+ external resources Delete: will delete the external resources
+ Archive: will archive the external resources, if it supports that'
+ enum:
+ - Delete
+ - Retain
+ - Archive
+ type: string
deployKeys:
additionalProperties:
description: DeployKey defines an SSH key to be used for git operations.
diff --git a/docs/modules/ROOT/assets/images/gitlab_settings.png b/docs/modules/ROOT/assets/images/gitlab_settings.png
new file mode 100644
index 00000000..00eac9fb
Binary files /dev/null and b/docs/modules/ROOT/assets/images/gitlab_settings.png differ
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index 259dda27..bdde2e09 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -10,4 +10,4 @@ include::partial$nav-howtos.adoc[]
include::partial$nav-reference.adoc[]
.Explanation
-include::partial$nav-explanation.adoc[]
\ No newline at end of file
+include::partial$nav-explanation.adoc[]
diff --git a/docs/modules/ROOT/pages/configuration.adoc b/docs/modules/ROOT/pages/configuration.adoc
new file mode 100644
index 00000000..73ad1d8a
--- /dev/null
+++ b/docs/modules/ROOT/pages/configuration.adoc
@@ -0,0 +1,50 @@
+= Configuration
+
+== Permissions for other systems
+
+In order for the operator to work correctly it will need specific permissions in other systems.
+
+=== Gitlab
+
+These are the settings needed for the Gitlab API token.
+
+image::gitlab_settings.png[]
+
+=== Vault
+[source,hcl]
+----
+path "kv/data/*" {
+ capabilities = ["read", "create", "update", "delete"]
+}
+
+path "kv/metadata/*" {
+ capabilities = ["read", "create", "update", "delete", "list"]
+}
+
+path "kv/delete/*" {
+ capabilities = ["update"]
+}
+----
+
+== Environment variables
+
+[cols=",",options="header",]
+|===
+
+a| Environment Variable
+
+a| Description
+
+| VAULT_ADDR | Sets the address to the Vault instance
+
+| VAULT_TOKEN | Sets the Vault token to be used, ony recommended for testing. in production the K8s authentication should be used.
+
+| SKIP_VAULT_SETUP | Doesn't create any Vault secrets. Recommended for testing only.
+
+| DEFAULT_DELETION_POLICY | Sets what deletion policy for external resources (Git, Vault) should be used by default.
+
+| LIEUTENANT_SYNC_DURATION | Defines with what frequence the CRs will be synced. Default: 5m
+
+| LIEUTENANT_DELETE_PROTECTION | Defines whether the annotation to protect for accidental deletion should be set by default. Default: true
+
+|===
diff --git a/docs/modules/ROOT/partials/crds.html b/docs/modules/ROOT/partials/crds.html
index 4c7f7a34..d7b6c0bf 100644
--- a/docs/modules/ROOT/partials/crds.html
+++ b/docs/modules/ROOT/partials/crds.html
@@ -236,6 +236,26 @@
+
+
+
+deletionPolicy
+
+
+DeletionPolicy
+
+
+
+ |
+
+
+ DeletionPolicy defines how the external resources should be treated upon CR deletion.
+Retain: will not delete any external resources
+Delete: will delete the external resources
+Archive: will archive the external resources, if it supports that
+
+ |
+
@@ -386,6 +406,26 @@
+
+
+
+deletionPolicy
+
+
+DeletionPolicy
+
+
+
+ |
+
+
+ DeletionPolicy defines how the external resources should be treated upon CR deletion.
+Retain: will not delete any external resources
+Delete: will delete the external resources
+Archive: will archive the external resources, if it supports that
+
+ |
+
+(Appears on:
+ClusterSpec,
+GitRepoTemplate,
+TenantSpec)
+
+
+
DeletionPolicy defines the type deletion policy
+
DeployKey
@@ -882,6 +933,26 @@
GitRepoTemplate
+
+
+
+deletionPolicy
+
+
+DeletionPolicy
+
+
+
+ |
+
+
+ DeletionPolicy defines how the external resources should be treated upon CR deletion.
+Retain: will not delete any external resources
+Delete: will delete the external resources
+Archive: will archive the external resources, if it supports that
+
+ |
+
GitType
@@ -996,6 +1067,26 @@ Tenant
+
+
+
+deletionPolicy
+
+
+DeletionPolicy
+
+
+
+ |
+
+
+ DeletionPolicy defines how the external resources should be treated upon CR deletion.
+Retain: will not delete any external resources
+Delete: will delete the external resources
+Archive: will archive the external resources, if it supports that
+
+ |
+
@@ -1082,6 +1173,26 @@
TenantSpec
+
+
+
+deletionPolicy
+
+
+DeletionPolicy
+
+
+
+ |
+
+
+ DeletionPolicy defines how the external resources should be treated upon CR deletion.
+Retain: will not delete any external resources
+Delete: will delete the external resources
+Archive: will archive the external resources, if it supports that
+
+ |
+
TenantStatus
diff --git a/examples/cluster.yaml b/examples/cluster.yaml
index 95e607cd..65ac1af2 100644
--- a/examples/cluster.yaml
+++ b/examples/cluster.yaml
@@ -2,11 +2,15 @@ apiVersion: syn.tools/v1alpha1
kind: Cluster
metadata:
name: c-ae3oso
+ annotations:
+ syn.tools/protected-delete: "false"
spec:
displayName: Big Corp. Production Cluster
+ deletionPolicy: Delete
gitRepoTemplate:
path: cluster
repoName: cluster1
+ deletionPolicy: Delete
apiSecretRef:
name: example-secret
# namespace: syn-lieutenant
diff --git a/examples/gitrepo-secret.yaml b/examples/gitrepo-secret.yaml
index cf2b6139..47cef88e 100644
--- a/examples/gitrepo-secret.yaml
+++ b/examples/gitrepo-secret.yaml
@@ -1,7 +1,7 @@
apiVersion: v1
stringData:
endpoint: http://192.168.5.42:8080
- token: zbxUWoPykEh5ZjG-mFsa
+ token: vY3gHvPs82NvYK8dKAGw
kind: Secret
metadata:
name: example-secret
diff --git a/examples/tenant.yaml b/examples/tenant.yaml
index 15160d8d..c1562cdb 100644
--- a/examples/tenant.yaml
+++ b/examples/tenant.yaml
@@ -7,6 +7,7 @@ spec:
gitRepoTemplate:
path: tenant
repoName: tenant1
+ deletionPolicy: Delete
apiSecretRef:
name: example-secret
# namespace: syn-lieutenant
diff --git a/go.sum b/go.sum
index 9e16a427..fa4315da 100644
--- a/go.sum
+++ b/go.sum
@@ -943,6 +943,7 @@ go.elastic.co/apm/module/apmot v1.5.0/go.mod h1:d2KYwhJParTpyw2WnTNy8geNlHKKFX+4
go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHtchs=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -1403,4 +1404,5 @@ sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1
sigs.k8s.io/structured-merge-diff v1.0.2/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc h1:MksmcCZQWAQJCTA5T0jgI/0sJ51AVm4Z41MrmfczEoc=
vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI=
diff --git a/pkg/apis/syn/v1alpha1/cluster_types.go b/pkg/apis/syn/v1alpha1/cluster_types.go
index 0d9b1bcb..b26f2ca2 100644
--- a/pkg/apis/syn/v1alpha1/cluster_types.go
+++ b/pkg/apis/syn/v1alpha1/cluster_types.go
@@ -24,6 +24,12 @@ type ClusterSpec struct {
TokenLifeTime string `json:"tokenLifeTime,omitempty"`
// Facts are key/value pairs for statically configured facts
Facts *Facts `json:"facts,omitempty"`
+ // DeletionPolicy defines how the external resources should be treated upon CR deletion.
+ // Retain: will not delete any external resources
+ // Delete: will delete the external resources
+ // Archive: will archive the external resources, if it supports that
+ // +kubebuilder:validation:Enum=Delete;Retain;Archive
+ DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
}
// BootstrapToken this key is used only once for Steward to register.
diff --git a/pkg/apis/syn/v1alpha1/gitrepo_types.go b/pkg/apis/syn/v1alpha1/gitrepo_types.go
index 11ce6894..e8a83919 100644
--- a/pkg/apis/syn/v1alpha1/gitrepo_types.go
+++ b/pkg/apis/syn/v1alpha1/gitrepo_types.go
@@ -24,9 +24,12 @@ const (
// GitPhase enum values
const (
- Created = GitPhase("created")
- Failed = GitPhase("failed")
- PhaseUnknown = GitPhase("")
+ Created GitPhase = "created"
+ Failed GitPhase = "failed"
+ PhaseUnknown GitPhase = ""
+ ArchivePolicy DeletionPolicy = "Archive"
+ DeletePolicy DeletionPolicy = "Delete"
+ RetainPolicy DeletionPolicy = "Retain"
)
// GitPhase is the enum for the git phase status
@@ -38,6 +41,9 @@ type GitType string
// RepoType specifies the type of the repo
type RepoType string
+// DeletionPolicy defines the type deletion policy
+type DeletionPolicy string
+
// GitRepoSpec defines the desired state of GitRepo
type GitRepoSpec struct {
GitRepoTemplate `json:",inline"`
@@ -64,6 +70,12 @@ type GitRepoTemplate struct {
// TemplateFiles is a list of files that should be pushed to the repository
// after its creation.
TemplateFiles map[string]string `json:"templateFiles,omitempty"`
+ // DeletionPolicy defines how the external resources should be treated upon CR deletion.
+ // Retain: will not delete any external resources
+ // Delete: will delete the external resources
+ // Archive: will archive the external resources, if it supports that
+ // +kubebuilder:validation:Enum=Delete;Retain;Archive
+ DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
}
// DeployKey defines an SSH key to be used for git operations.
diff --git a/pkg/apis/syn/v1alpha1/tenant_types.go b/pkg/apis/syn/v1alpha1/tenant_types.go
index b3a2f9ba..3d051a19 100644
--- a/pkg/apis/syn/v1alpha1/tenant_types.go
+++ b/pkg/apis/syn/v1alpha1/tenant_types.go
@@ -12,6 +12,12 @@ type TenantSpec struct {
GitRepoURL string `json:"gitRepoURL,omitempty"`
// GitRepoTemplate Template for managing the GitRepo object. If not set, no GitRepo object will be created.
GitRepoTemplate *GitRepoTemplate `json:"gitRepoTemplate,omitempty"`
+ // DeletionPolicy defines how the external resources should be treated upon CR deletion.
+ // Retain: will not delete any external resources
+ // Delete: will delete the external resources
+ // Archive: will archive the external resources, if it supports that
+ // +kubebuilder:validation:Enum=Delete;Retain;Archive
+ DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
}
// TenantStatus defines the observed state of Tenant
diff --git a/pkg/controller/cluster/cluster_reconcile.go b/pkg/controller/cluster/cluster_reconcile.go
index 9afcf9ea..8e085d47 100644
--- a/pkg/controller/cluster/cluster_reconcile.go
+++ b/pkg/controller/cluster/cluster_reconcile.go
@@ -11,8 +11,10 @@ import (
"strings"
"time"
+ "github.com/go-logr/logr"
synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
synTenant "github.com/projectsyn/lieutenant-operator/pkg/controller/tenant"
+ "github.com/projectsyn/lieutenant-operator/pkg/git/manager"
"github.com/projectsyn/lieutenant-operator/pkg/helpers"
"github.com/projectsyn/lieutenant-operator/pkg/vault"
corev1 "k8s.io/api/core/v1"
@@ -21,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
@@ -28,6 +31,7 @@ const (
clusterClassContent = `classes:
- %s.%s
`
+ finalizerName = "cluster.lieutenant.syn.tools"
)
// Reconcile reads that state of the cluster for a Cluster object and makes changes based on the state read
@@ -38,82 +42,119 @@ func (r *ReconcileCluster) Reconcile(request reconcile.Request) (reconcile.Resul
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling Cluster")
- instance := &synv1alpha1.Cluster{}
+ err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
- err := r.client.Get(context.TODO(), request.NamespacedName, instance)
- if err != nil {
- if errors.IsNotFound(err) {
- return reconcile.Result{}, nil
- }
- return reconcile.Result{}, err
- }
-
- if err := r.createClusterRBAC(*instance); err != nil {
- return reconcile.Result{}, err
- }
+ instance := &synv1alpha1.Cluster{}
- if instance.Status.BootstrapToken == nil {
- reqLogger.Info("Adding status to Cluster object")
- err := r.newStatus(instance)
+ err := r.client.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
- return reconcile.Result{}, err
+ if errors.IsNotFound(err) {
+ return nil
+ }
+ return err
}
- }
- if time.Now().After(instance.Status.BootstrapToken.ValidUntil.Time) {
- instance.Status.BootstrapToken.TokenValid = false
- }
+ if err := r.createClusterRBAC(*instance); err != nil {
+ return err
+ }
- gvk := schema.GroupVersionKind{
- Version: instance.APIVersion,
- Kind: instance.Kind,
- }
+ if instance.Status.BootstrapToken == nil {
+ reqLogger.Info("Adding status to Cluster object")
+ err := r.newStatus(instance)
+ if err != nil {
+ return err
+ }
+ }
- if len(instance.Spec.GitRepoTemplate.DisplayName) == 0 {
- instance.Spec.GitRepoTemplate.DisplayName = instance.Spec.DisplayName
- }
+ if time.Now().After(instance.Status.BootstrapToken.ValidUntil.Time) {
+ instance.Status.BootstrapToken.TokenValid = false
+ }
- err = helpers.CreateOrUpdateGitRepo(instance, gvk, instance.Spec.GitRepoTemplate, r.client, instance.Spec.TenantRef)
- if err != nil {
- reqLogger.Error(err, "Cannot create or update git repo object")
- return reconcile.Result{}, err
- }
+ gvk := schema.GroupVersionKind{
+ Version: instance.APIVersion,
+ Kind: instance.Kind,
+ }
- repoName := request.NamespacedName
- repoName.Name = instance.Spec.TenantRef.Name
+ if len(instance.Spec.GitRepoTemplate.DisplayName) == 0 {
+ instance.Spec.GitRepoTemplate.DisplayName = instance.Spec.DisplayName
+ }
+
+ instance.Spec.GitRepoTemplate.DeletionPolicy = instance.Spec.DeletionPolicy
- if strings.ToLower(os.Getenv("SKIP_VAULT_SETUP")) != "true" {
- client, err := vault.NewClient()
+ err = helpers.CreateOrUpdateGitRepo(instance, gvk, instance.Spec.GitRepoTemplate, r.client, instance.Spec.TenantRef)
if err != nil {
- return reconcile.Result{}, err
+ reqLogger.Error(err, "Cannot create or update git repo object")
+ return err
}
- token, err := r.getServiceAccountToken(instance)
+ repoName := request.NamespacedName
+ repoName.Name = instance.Spec.TenantRef.Name
+
+ var vaultClient vault.VaultClient = nil
+ secretPath := path.Join(instance.Spec.TenantRef.Name, instance.Name, "steward")
+
+ deletionPolicy := instance.Spec.DeletionPolicy
+ if deletionPolicy == "" {
+ deletionPolicy = helpers.GetDeletionPolicy()
+ }
+
+ vaultClient, err = vault.NewClient(deletionPolicy, reqLogger)
if err != nil {
- return reconcile.Result{}, err
+ return err
+ }
+
+ if strings.ToLower(os.Getenv("SKIP_VAULT_SETUP")) != "true" {
+
+ token, err := r.getServiceAccountToken(instance)
+ if err != nil {
+ return err
+ }
+
+ err = vaultClient.AddSecrets([]vault.VaultSecret{{Path: secretPath, Value: token}})
+ if err != nil {
+ return err
+ }
+
+ }
+
+ deleted := helpers.HandleDeletion(instance, finalizerName, r.client)
+ if deleted.FinalizerRemoved {
+ if vaultClient != nil {
+ err := vaultClient.RemoveSecrets([]vault.VaultSecret{{Path: path.Dir(secretPath), Value: ""}})
+ if err != nil {
+ return err
+ }
+ }
+ err = r.removeClusterFileFromTenant(instance.GetName(), repoName, reqLogger)
+ if err != nil {
+ return err
+ }
+ }
+ if deleted.Deleted {
+ return r.client.Update(context.TODO(), instance)
}
- err = client.SetToken(path.Join(instance.Spec.TenantRef.Name, instance.Name, "steward"), token, reqLogger)
+ err = r.updateTenantGitRepo(repoName, instance.GetName())
if err != nil {
- return reconcile.Result{}, err
+ return err
}
- }
- err = r.updateTenantGitRepo(repoName, instance.GetName())
- if err != nil {
- return reconcile.Result{}, err
- }
+ helpers.AddTenantLabel(&instance.ObjectMeta, instance.Spec.TenantRef.Name)
+ helpers.AddDeletionProtection(instance)
+ controllerutil.AddFinalizer(instance, finalizerName)
- helpers.AddTenantLabel(&instance.ObjectMeta, instance.Spec.TenantRef.Name)
- instance.Spec.GitRepoURL, instance.Spec.GitHostKeys, err = helpers.GetGitRepoURLAndHostKeys(instance, r.client)
- if err != nil {
- return reconcile.Result{}, err
- }
- err = r.client.Status().Update(context.TODO(), instance)
- if err != nil {
- return reconcile.Result{}, err
- }
- return reconcile.Result{}, r.client.Update(context.TODO(), instance)
+ instance.Spec.GitRepoURL, instance.Spec.GitHostKeys, err = helpers.GetGitRepoURLAndHostKeys(instance, r.client)
+ if err != nil {
+ return err
+ }
+ err = r.client.Status().Update(context.TODO(), instance)
+ if err != nil {
+ return err
+ }
+ return r.client.Update(context.TODO(), instance)
+ })
+
+ return reconcile.Result{}, err
}
func (r *ReconcileCluster) generateToken() (string, error) {
@@ -153,11 +194,15 @@ func (r *ReconcileCluster) newStatus(cluster *synv1alpha1.Cluster) error {
return nil
}
+func (r *ReconcileCluster) getTenantCR(tenant types.NamespacedName) (*synv1alpha1.Tenant, error) {
+ tenantCR := &synv1alpha1.Tenant{}
+ return tenantCR, r.client.Get(context.TODO(), tenant, tenantCR)
+}
+
func (r *ReconcileCluster) updateTenantGitRepo(tenant types.NamespacedName, clusterName string) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
- tenantCR := &synv1alpha1.Tenant{}
- err := r.client.Get(context.TODO(), tenant, tenantCR)
+ tenantCR, err := r.getTenantCR(tenant)
if err != nil {
return err
}
@@ -205,3 +250,27 @@ func (r *ReconcileCluster) getServiceAccountToken(instance metav1.Object) (strin
return "", fmt.Errorf("no matching secrets found")
}
+
+func (r *ReconcileCluster) removeClusterFileFromTenant(clusterName string, tenantInfo types.NamespacedName, reqLogger logr.Logger) error {
+
+ tenantCR, err := r.getTenantCR(tenantInfo)
+ if err != nil {
+ return err
+ }
+
+ fileName := clusterName + ".yml"
+
+ if tenantCR.Spec.GitRepoTemplate.TemplateFiles == nil {
+ return nil
+ }
+
+ if _, ok := tenantCR.Spec.GitRepoTemplate.TemplateFiles[fileName]; ok {
+ tenantCR.Spec.GitRepoTemplate.TemplateFiles[fileName] = manager.DeletionMagicString
+ err := r.client.Update(context.TODO(), tenantCR)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/controller/cluster/cluster_reconcile_test.go b/pkg/controller/cluster/cluster_reconcile_test.go
index 61234cad..58cca2c4 100644
--- a/pkg/controller/cluster/cluster_reconcile_test.go
+++ b/pkg/controller/cluster/cluster_reconcile_test.go
@@ -4,12 +4,12 @@ import (
"context"
"fmt"
"os"
+ "path"
"reflect"
"strconv"
"testing"
"time"
- "github.com/go-logr/logr"
"github.com/projectsyn/lieutenant-operator/pkg/apis"
synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
"github.com/projectsyn/lieutenant-operator/pkg/controller/tenant"
@@ -176,11 +176,12 @@ func TestReconcileCluster_Reconcile(t *testing.T) {
assert.NoError(t, err)
if tt.skipVault {
- assert.Empty(t, testMockClient.token)
+ assert.Empty(t, testMockClient.secrets)
} else {
saToken, err := r.getServiceAccountToken(newCluster)
+ saSecrets := []vault.VaultSecret{{Value: saToken, Path: path.Join(tt.fields.tenantName, tt.fields.objName, "steward")}}
assert.NoError(t, err)
- assert.Equal(t, testMockClient.token, saToken)
+ assert.Equal(t, testMockClient.secrets, saSecrets)
}
role := &rbacv1.Role{}
err = cl.Get(context.TODO(), req.NamespacedName, role)
@@ -204,11 +205,15 @@ func TestReconcileCluster_Reconcile(t *testing.T) {
}
type TestMockClient struct {
- token string
+ secrets []vault.VaultSecret
}
-func (m *TestMockClient) SetToken(secretPath, token string, log logr.Logger) error {
- m.token = token
+func (m *TestMockClient) AddSecrets(secrets []vault.VaultSecret) error {
+ m.secrets = secrets
+ return nil
+}
+
+func (m *TestMockClient) RemoveSecrets(secrets []vault.VaultSecret) error {
return nil
}
diff --git a/pkg/controller/gitrepo/gitrepo_reconcile.go b/pkg/controller/gitrepo/gitrepo_reconcile.go
index ea908284..363f390b 100644
--- a/pkg/controller/gitrepo/gitrepo_reconcile.go
+++ b/pkg/controller/gitrepo/gitrepo_reconcile.go
@@ -3,26 +3,19 @@ package gitrepo
import (
"context"
"fmt"
- "net/url"
"github.com/projectsyn/lieutenant-operator/pkg/git/manager"
"github.com/projectsyn/lieutenant-operator/pkg/helpers"
synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
- corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
- "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const (
- // SecretTokenName is the name of the secret entry containing the token
- SecretTokenName = "token"
- // SecretHostKeysName is the name of the secret entry containing the SSH host keys
- SecretHostKeysName = "hostKeys"
- // SecretEndpointName is the name of the secret entry containing the api endpoint
- SecretEndpointName = "endpoint"
+ finalizerName = "gitrepo.lieutenant.syn.tools"
)
// Reconcile will create or delete a git repository based on the event.
@@ -46,64 +39,15 @@ func (r *ReconcileGitRepo) Reconcile(request reconcile.Request) (reconcile.Resul
return err
}
- secret := &corev1.Secret{}
- namespacedName := types.NamespacedName{
- Name: instance.Spec.APISecretRef.Name,
- Namespace: instance.Namespace,
- }
-
- if len(instance.Spec.APISecretRef.Namespace) > 0 {
- namespacedName.Namespace = instance.Spec.APISecretRef.Namespace
- }
-
- err = r.client.Get(context.TODO(), namespacedName, secret)
- if err != nil {
- return fmt.Errorf("error getting git secret: %v", err)
- }
-
- if hostKeys, ok := secret.Data[SecretHostKeysName]; ok {
- instance.Status.HostKeys = string(hostKeys)
- }
-
- if _, ok := secret.Data[SecretEndpointName]; !ok {
- return fmt.Errorf("secret %s does not contain endpoint data", secret.GetName())
- }
-
- if _, ok := secret.Data[SecretTokenName]; !ok {
- return fmt.Errorf("secret %s does not contain token", secret.GetName())
- }
-
- repoURL, err := url.Parse(string(secret.Data[SecretEndpointName]) + "/" + instance.Spec.Path + "/" + instance.Spec.RepoName)
-
+ repo, hostKeys, err := manager.GetGitClient(&instance.Spec.GitRepoTemplate, instance.GetNamespace(), reqLogger, r.client)
if err != nil {
return err
}
- repoOptions := manager.RepoOptions{
- Credentials: manager.Credentials{
- Token: string(secret.Data[SecretTokenName]),
- },
- DeployKeys: instance.Spec.DeployKeys,
- Logger: reqLogger,
- Path: instance.Spec.Path,
- RepoName: instance.Spec.RepoName,
- DisplayName: instance.Spec.DisplayName,
- URL: repoURL,
- TemplateFiles: instance.Spec.TemplateFiles,
- }
-
- repo, err := manager.NewRepo(repoOptions)
- if err != nil {
- return err
- }
-
- err = repo.Connect()
- if err != nil {
- return err
- }
+ instance.Status.HostKeys = hostKeys
if !r.repoExists(repo) {
- reqLogger.Info("creating git repo", SecretEndpointName, repoOptions.URL)
+ reqLogger.Info("creating git repo", manager.SecretEndpointName, repo.FullURL())
err := repo.Create()
if err != nil {
return r.handleRepoError(err, instance, repo)
@@ -111,6 +55,17 @@ func (r *ReconcileGitRepo) Reconcile(request reconcile.Request) (reconcile.Resul
reqLogger.Info("successfully created the repository")
}
+ deleted := helpers.HandleDeletion(instance, finalizerName, r.client)
+ if deleted.FinalizerRemoved {
+ err := repo.Remove()
+ if err != nil {
+ return err
+ }
+ }
+ if deleted.Deleted {
+ return r.client.Update(context.TODO(), instance)
+ }
+
err = repo.CommitTemplateFiles()
if err != nil {
return r.handleRepoError(err, instance, repo)
@@ -126,6 +81,9 @@ func (r *ReconcileGitRepo) Reconcile(request reconcile.Request) (reconcile.Resul
}
helpers.AddTenantLabel(&instance.ObjectMeta, instance.Spec.TenantRef.Name)
+ helpers.AddDeletionProtection(instance)
+
+ controllerutil.AddFinalizer(instance, finalizerName)
err = r.client.Update(context.TODO(), instance)
if err != nil {
diff --git a/pkg/controller/gitrepo/gitrepo_reconcile_test.go b/pkg/controller/gitrepo/gitrepo_reconcile_test.go
index 1f6fd88b..293dedfd 100644
--- a/pkg/controller/gitrepo/gitrepo_reconcile_test.go
+++ b/pkg/controller/gitrepo/gitrepo_reconcile_test.go
@@ -102,9 +102,9 @@ func TestReconcileGitRepo_Reconcile(t *testing.T) {
Namespace: tt.fields.namespace,
},
Data: map[string][]byte{
- SecretEndpointName: []byte(tt.fields.URL),
- SecretTokenName: []byte("secret"),
- SecretHostKeysName: []byte("somekey"),
+ manager.SecretEndpointName: []byte(tt.fields.URL),
+ manager.SecretTokenName: []byte("secret"),
+ manager.SecretHostKeysName: []byte("somekey"),
},
}
@@ -134,7 +134,7 @@ func TestReconcileGitRepo_Reconcile(t *testing.T) {
err = cl.Get(context.TODO(), req.NamespacedName, gitRepo)
assert.NoError(t, err)
if !tt.wantErr {
- assert.Equal(t, string(secret.Data[SecretHostKeysName]), gitRepo.Status.HostKeys)
+ assert.Equal(t, string(secret.Data[manager.SecretHostKeysName]), gitRepo.Status.HostKeys)
assert.Equal(t, synv1alpha1.DefaultRepoType, gitRepo.Spec.RepoType)
assert.Equal(t, tt.fields.displayName, gitRepo.Spec.DisplayName)
assert.Equal(t, tt.fields.displayName, savedGitRepoOpt.DisplayName)
@@ -161,6 +161,14 @@ func (t testRepoImplementation) New(options manager.RepoOptions) (manager.Repo,
return t, nil
}
+func (t testRepoImplementation) RemoveFile(path string) error {
+ return nil
+}
+
+func (t testRepoImplementation) Remove() error {
+ return nil
+}
+
func (t testRepoImplementation) Type() string {
return "test"
}
diff --git a/pkg/controller/tenant/tenant_reconcile.go b/pkg/controller/tenant/tenant_reconcile.go
index b742a448..9d169968 100644
--- a/pkg/controller/tenant/tenant_reconcile.go
+++ b/pkg/controller/tenant/tenant_reconcile.go
@@ -24,47 +24,52 @@ func (r *ReconcileTenant) Reconcile(request reconcile.Request) (reconcile.Result
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling Tenant")
- // Fetch the Tenant instance
- instance := &synv1alpha1.Tenant{}
- err := r.client.Get(context.TODO(), request.NamespacedName, instance)
- if err != nil {
- if errors.IsNotFound(err) {
- // Request object not found, could have been deleted after reconcile request.
- // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
- // Return and don't requeue
- return reconcile.Result{}, nil
+ err := retry.OnError(retry.DefaultBackoff, errors.IsNotFound, func() error {
+ // Fetch the Tenant instance
+ instance := &synv1alpha1.Tenant{}
+ err := r.client.Get(context.TODO(), request.NamespacedName, instance)
+ if err != nil {
+ if errors.IsNotFound(err) {
+ // Request object not found, could have been deleted after reconcile request.
+ // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
+ // Return and don't requeue
+ return nil
+ }
+ // Error reading the object - requeue the request.
+ return err
}
- // Error reading the object - requeue the request.
- return reconcile.Result{}, err
- }
- gvk := schema.GroupVersionKind{
- Version: instance.APIVersion,
- Kind: instance.Kind,
- }
+ gvk := schema.GroupVersionKind{
+ Version: instance.APIVersion,
+ Kind: instance.Kind,
+ }
+
+ if len(instance.Spec.GitRepoTemplate.DisplayName) == 0 {
+ instance.Spec.GitRepoTemplate.DisplayName = instance.Spec.DisplayName
+ }
- if len(instance.Spec.GitRepoTemplate.DisplayName) == 0 {
- instance.Spec.GitRepoTemplate.DisplayName = instance.Spec.DisplayName
- }
+ commonClassFile := CommonClassName + ".yml"
+ if instance.Spec.GitRepoTemplate.TemplateFiles == nil {
+ instance.Spec.GitRepoTemplate.TemplateFiles = map[string]string{}
+ }
+ if _, ok := instance.Spec.GitRepoTemplate.TemplateFiles[commonClassFile]; !ok {
+ instance.Spec.GitRepoTemplate.TemplateFiles[commonClassFile] = ""
+ }
- commonClassFile := CommonClassName + ".yml"
- if instance.Spec.GitRepoTemplate.TemplateFiles == nil {
- instance.Spec.GitRepoTemplate.TemplateFiles = map[string]string{}
- }
- if _, ok := instance.Spec.GitRepoTemplate.TemplateFiles[commonClassFile]; !ok {
- instance.Spec.GitRepoTemplate.TemplateFiles[commonClassFile] = ""
- }
+ instance.Spec.GitRepoTemplate.DeletionPolicy = instance.Spec.DeletionPolicy
+
+ err = helpers.CreateOrUpdateGitRepo(instance, gvk, instance.Spec.GitRepoTemplate, r.client, corev1.LocalObjectReference{Name: instance.GetName()})
+ if err != nil {
+ return err
+ }
- err = helpers.CreateOrUpdateGitRepo(instance, gvk, instance.Spec.GitRepoTemplate, r.client, corev1.LocalObjectReference{Name: instance.GetName()})
- if err != nil {
- return reconcile.Result{}, err
- }
- err = retry.OnError(retry.DefaultBackoff, errors.IsNotFound, func() error {
instance.Spec.GitRepoURL, _, err = helpers.GetGitRepoURLAndHostKeys(instance, r.client)
- return err
+ if err != nil {
+ return err
+ }
+
+ helpers.AddDeletionProtection(instance)
+ return r.client.Update(context.TODO(), instance)
})
- if err != nil {
- return reconcile.Result{}, err
- }
- return reconcile.Result{}, r.client.Update(context.TODO(), instance)
+ return reconcile.Result{}, err
}
diff --git a/pkg/git/gitlab/gitlab.go b/pkg/git/gitlab/gitlab.go
index 9c1b89ee..4b81e5f6 100644
--- a/pkg/git/gitlab/gitlab.go
+++ b/pkg/git/gitlab/gitlab.go
@@ -21,7 +21,8 @@ func init() {
manager.Register(&Gitlab{})
}
-// Gitlab holds the necessary information to communincate with a Gitlab server
+// Gitlab holds the necessary information to communincate with a Gitlab server.
+// Each Gitlab instance will handle exactly one project.
type Gitlab struct {
client *gitlab.Client
credentials manager.Credentials
@@ -141,8 +142,42 @@ func (g *Gitlab) removeDeployKeys(deleteKeys map[string]synv1alpha1.DeployKey) e
return err
}
-// Delete deletes the project handled by the gitlab instance
-func (g *Gitlab) Delete() error {
+// Remove removes the project according to the recycle policy.
+// Delete -> project gets deleted
+// Archive -> project gets archived
+// Retain -> nothing happens
+func (g *Gitlab) Remove() error {
+ switch g.ops.DeletionPolicy {
+ case synv1alpha1.DeletePolicy:
+ g.log.Info("deleting", "project", g.project.Name)
+ return g.delete()
+ case synv1alpha1.ArchivePolicy:
+ g.log.Info("archiving", "project", g.project.Name)
+ return g.archive()
+ default:
+ g.log.Info("retaining", "project", g.project.Name)
+ return nil
+ }
+}
+
+// archive archives the project handled by this gitlab instance
+func (g *Gitlab) archive() error {
+ err := g.getProject()
+ if err != nil {
+ return err
+ }
+
+ if g.project == nil {
+ return fmt.Errorf("no project %v found, can't archive", g.ops.Path)
+ }
+
+ _, _, err = g.client.Projects.ArchiveProject(g.project.ID)
+
+ return err
+}
+
+// delete deletes the project handled by the gitlab instance
+func (g *Gitlab) delete() error {
// make sure to have the latest version of the project
err := g.getProject()
if err != nil {
@@ -316,34 +351,35 @@ func (g *Gitlab) CommitTemplateFiles() error {
return nil
}
- filesToApply, err := g.compareFiles()
+ filesToCommit, err := g.compareFiles()
if err != nil {
return err
}
- if len(filesToApply) == 0 {
+ if len(filesToCommit) == 0 {
// we're done here
return nil
}
g.log.Info("populating repository with template files")
- co := &gitlab.CreateCommitOptions{
- AuthorEmail: builtinx.NewString("lieutenant-operator@syn.local"),
- AuthorName: builtinx.NewString("Lieutenant Operator"),
- Branch: builtinx.NewString("master"),
- CommitMessage: builtinx.NewString("Provision templates"),
- }
+ co := g.getCommitOptions()
- co.Actions = []*gitlab.CommitAction{}
+ for _, file := range filesToCommit {
+ action := &gitlab.CommitAction{
+ FilePath: file.FileName,
+ Content: file.Content,
+ }
- for name, content := range filesToApply {
+ if file.Delete {
+ g.log.Info("deleting file from repository", "file", action.FilePath, "repository", g.project.Name)
+ action.Action = gitlab.FileDelete
+ } else {
+ g.log.Info("writing file to repository", "file", action.FilePath, "repository", g.project.Name)
+ action.Action = gitlab.FileCreate
+ }
- co.Actions = append(co.Actions, &gitlab.CommitAction{
- Action: gitlab.FileCreate,
- FilePath: name,
- Content: content,
- })
+ co.Actions = append(co.Actions, action)
}
_, _, err = g.client.Commits.CreateCommit(g.project.ID, co, nil)
@@ -354,29 +390,62 @@ func (g *Gitlab) CommitTemplateFiles() error {
// compareFiles will compare the files of the repositories root with the
// files that should be created. If there are existing files they will be
// dropped.
-func (g *Gitlab) compareFiles() (map[string]string, error) {
+func (g *Gitlab) compareFiles() ([]manager.CommitFile, error) {
- newmap := map[string]string{}
+ files := []manager.CommitFile{}
trees, _, err := g.client.Repositories.ListTree(g.project.ID, nil, nil)
if err != nil {
// if the tree is not found it's probably just because there are no files at all currently...
+ // So we have to apply all pending ones.
if strings.Contains(err.Error(), "Tree Not Found") {
- return g.ops.TemplateFiles, nil
+
+ for name, content := range g.ops.TemplateFiles {
+ files = append(files, manager.CommitFile{
+ FileName: name,
+ Content: content,
+ })
+ }
+
+ return files, nil
}
- return newmap, fmt.Errorf("cannot list files in repository: %s", err)
+ return files, fmt.Errorf("cannot list files in repository: %s", err)
}
- treeMap := map[string]bool{}
+ compareMap := map[string]bool{}
for _, tree := range trees {
- treeMap[tree.Path] = true
+ compareMap[tree.Path] = true
+ }
+
+ for name, content := range g.ops.TemplateFiles {
+ if _, ok := compareMap[name]; ok && content == manager.DeletionMagicString {
+ files = append(files, manager.CommitFile{
+ FileName: name,
+ Content: content,
+ Delete: true,
+ })
+ } else if !ok && content != manager.DeletionMagicString {
+ files = append(files, manager.CommitFile{
+ FileName: name,
+ Content: content,
+ })
+ }
+
}
- for k, v := range g.ops.TemplateFiles {
- if _, ok := treeMap[k]; !ok {
- newmap[k] = v
- }
+ return files, nil
+}
+
+func (g *Gitlab) getCommitOptions() *gitlab.CreateCommitOptions {
+
+ co := &gitlab.CreateCommitOptions{
+ AuthorEmail: builtinx.NewString("lieutenant-operator@syn.local"),
+ AuthorName: builtinx.NewString("Lieutenant Operator"),
+ Branch: builtinx.NewString("master"),
+ CommitMessage: builtinx.NewString("Update cluster files"),
}
- return newmap, nil
+ co.Actions = []*gitlab.CommitAction{}
+
+ return co
}
diff --git a/pkg/git/gitlab/gitlab_test.go b/pkg/git/gitlab/gitlab_test.go
index b137ff27..d0f1721b 100644
--- a/pkg/git/gitlab/gitlab_test.go
+++ b/pkg/git/gitlab/gitlab_test.go
@@ -114,7 +114,7 @@ func testGetCreateServer() *httptest.Server {
mux.HandleFunc("/api/v4/projects", func(res http.ResponseWriter, req *http.Request) {
createProjectOptions := gitlab.CreateProjectOptions{}
buf := new(bytes.Buffer)
- buf.ReadFrom(req.Body)
+ _, _ = buf.ReadFrom(req.Body)
err := json.Unmarshal(buf.Bytes(), &createProjectOptions)
response := http.StatusOK
if err != nil {
@@ -244,7 +244,7 @@ func TestGitlab_Delete(t *testing.T) {
_ = g.Connect()
- if err := g.Delete(); (err != nil) != tt.wantErr {
+ if err := g.delete(); (err != nil) != tt.wantErr {
t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
}
})
@@ -274,7 +274,7 @@ func testGetUpdateServer(fail bool) *httptest.Server {
mux.HandleFunc("/api/v4/projects/3", func(res http.ResponseWriter, req *http.Request) {
editProjectOptions := gitlab.EditProjectOptions{}
buf := new(bytes.Buffer)
- buf.ReadFrom(req.Body)
+ _, _ = buf.ReadFrom(req.Body)
err := json.Unmarshal(buf.Bytes(), &editProjectOptions)
response := http.StatusOK
if err != nil {
diff --git a/pkg/git/manager/manager.go b/pkg/git/manager/manager.go
index 8e89d1e3..871230bd 100644
--- a/pkg/git/manager/manager.go
+++ b/pkg/git/manager/manager.go
@@ -1,14 +1,31 @@
package manager
import (
+ "context"
"fmt"
"net/url"
synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ corev1 "k8s.io/api/core/v1"
"github.com/go-logr/logr"
)
+const (
+ // SecretTokenName is the name of the secret entry containing the token
+ SecretTokenName = "token"
+ // SecretHostKeysName is the name of the secret entry containing the SSH host keys
+ SecretHostKeysName = "hostKeys"
+ // SecretEndpointName is the name of the secret entry containing the api endpoint
+ SecretEndpointName = "endpoint"
+ // DeletionMagicString defines when a file should be deleted from the repository
+ //TODO it will be replaced with somethin better in the futur TODO
+ DeletionMagicString = "{delete}"
+)
+
var (
// implementations holds each a copy of the registered Git implementation
implementations []Implementation
@@ -41,15 +58,17 @@ func NewRepo(opts RepoOptions) (Repo, error) {
// RepoOptions hold the options for creating a repository. The credentials are required to work. The deploykeys are
// optional but desired.
+// If not provided DeletionPolicy will default to archive.
type RepoOptions struct {
- Credentials Credentials
- DeployKeys map[string]synv1alpha1.DeployKey
- Logger logr.Logger
- URL *url.URL
- Path string
- RepoName string
- DisplayName string
- TemplateFiles map[string]string
+ Credentials Credentials
+ DeployKeys map[string]synv1alpha1.DeployKey
+ Logger logr.Logger
+ URL *url.URL
+ Path string
+ RepoName string
+ DisplayName string
+ TemplateFiles map[string]string
+ DeletionPolicy synv1alpha1.DeletionPolicy
}
// Credentials holds the authentication information for the API. Most of the times this
@@ -71,9 +90,12 @@ type Repo interface {
// Read will read the repository and populate it with the deployed keys. It will throw an
// error if the repo is not found on the server.
Read() error
- Delete() error
+ // Remove will remove the git project according to the recycle policy
+ Remove() error
Connect() error
- // CommitTemplateFiles uploads given files to the repository
+ // CommitTemplateFiles uploads given files to the repository.
+ // files that contain exactly the deletion magic string should be removed
+ // when calling this function. TODO: will be replaced with something better in the future.
CommitTemplateFiles() error
}
@@ -85,3 +107,74 @@ type Implementation interface {
// New returns a clean new Repo implementation with the given URL
New(options RepoOptions) (Repo, error)
}
+
+// CommitFile contains all information about a file that should be committed to git
+// TODO migrate to the CRDs in the future.
+type CommitFile struct {
+ FileName string
+ Content string
+ Delete bool
+}
+
+// GetGitClient will return a git client from a provided template. This does a lot more
+// plumbing than the simple NewClient() call. If you're needing a git client from a
+// reconcile function, this is the way to go.
+func GetGitClient(instance *synv1alpha1.GitRepoTemplate, namespace string, reqLogger logr.Logger, client client.Client) (Repo, string, error) {
+ secret := &corev1.Secret{}
+ namespacedName := types.NamespacedName{
+ Name: instance.APISecretRef.Name,
+ Namespace: namespace,
+ }
+
+ if len(instance.APISecretRef.Namespace) > 0 {
+ namespacedName.Namespace = instance.APISecretRef.Namespace
+ }
+
+ err := client.Get(context.TODO(), namespacedName, secret)
+ if err != nil {
+ return nil, "", fmt.Errorf("error getting git secret: %v", err)
+ }
+
+ hostKeysString := ""
+ if hostKeys, ok := secret.Data[SecretHostKeysName]; ok {
+ hostKeysString = string(hostKeys)
+ }
+
+ if _, ok := secret.Data[SecretEndpointName]; !ok {
+ return nil, "", fmt.Errorf("secret %s does not contain endpoint data", secret.GetName())
+ }
+
+ if _, ok := secret.Data[SecretTokenName]; !ok {
+ return nil, "", fmt.Errorf("secret %s does not contain token", secret.GetName())
+ }
+
+ repoURL, err := url.Parse(string(secret.Data[SecretEndpointName]) + "/" + instance.Path + "/" + instance.RepoName)
+
+ if err != nil {
+ return nil, "", err
+ }
+
+ repoOptions := RepoOptions{
+ Credentials: Credentials{
+ Token: string(secret.Data[SecretTokenName]),
+ },
+ DeployKeys: instance.DeployKeys,
+ Logger: reqLogger,
+ Path: instance.Path,
+ RepoName: instance.RepoName,
+ DisplayName: instance.DisplayName,
+ URL: repoURL,
+ TemplateFiles: instance.TemplateFiles,
+ DeletionPolicy: instance.DeletionPolicy,
+ }
+
+ repo, err := NewRepo(repoOptions)
+ if err != nil {
+ return nil, "", err
+ }
+
+ err = repo.Connect()
+
+ return repo, hostKeysString, err
+
+}
diff --git a/pkg/helpers/crd.go b/pkg/helpers/crd.go
index 3698d1e3..d68cb66c 100644
--- a/pkg/helpers/crd.go
+++ b/pkg/helpers/crd.go
@@ -3,18 +3,31 @@ package helpers
import (
"context"
"fmt"
+ "os"
+ "strconv"
corev1 "k8s.io/api/core/v1"
"github.com/projectsyn/lieutenant-operator/pkg/apis"
synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
+ "github.com/projectsyn/lieutenant-operator/pkg/git/manager"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
+const (
+ protectionSettingEnvVar = "LIEUTENANT_DELETE_PROTECTION"
+)
+
+type DeletionState struct {
+ FinalizerRemoved bool
+ Deleted bool
+}
+
// CreateOrUpdateGitRepo will create the gitRepo object if it doesn't already exist. If the owner object itself is a tenant tenantRef can be set nil.
func CreateOrUpdateGitRepo(obj metav1.Object, gvk schema.GroupVersionKind, template *synv1alpha1.GitRepoTemplate, client client.Client, tenantRef corev1.LocalObjectReference) error {
@@ -26,6 +39,10 @@ func CreateOrUpdateGitRepo(obj metav1.Object, gvk schema.GroupVersionKind, templ
return fmt.Errorf("the tenant name is empty")
}
+ if template.DeletionPolicy == "" {
+ template.DeletionPolicy = GetDeletionPolicy()
+ }
+
if template.RepoType == synv1alpha1.DefaultRepoType {
template.RepoType = synv1alpha1.AutoRepoType
}
@@ -44,6 +61,8 @@ func CreateOrUpdateGitRepo(obj metav1.Object, gvk schema.GroupVersionKind, templ
},
}
+ AddDeletionProtection(repo)
+
err := client.Create(context.TODO(), repo)
if err != nil && errors.IsAlreadyExists(err) {
existingRepo := &synv1alpha1.GitRepo{}
@@ -62,6 +81,13 @@ func CreateOrUpdateGitRepo(obj metav1.Object, gvk schema.GroupVersionKind, templ
err = client.Update(context.TODO(), existingRepo)
}
+
+ for file, content := range template.TemplateFiles {
+ if content == manager.DeletionMagicString {
+ delete(template.TemplateFiles, file)
+ }
+ }
+
return err
}
@@ -102,3 +128,71 @@ func (s SecretSortList) Less(i, j int) bool {
return s.Items[i].CreationTimestamp.Before(&s.Items[j].CreationTimestamp)
}
+
+// Checks if the slice of strings contains a specific string
+func SliceContainsString(list []string, s string) bool {
+ for _, v := range list {
+ if v == s {
+ return true
+ }
+ }
+ return false
+}
+
+// HandleDeletion will handle the finalizers if the object was deleted.
+// It will return true, if the finalizer was removed. If the object was
+// removed the reconcile can be returned.
+func HandleDeletion(instance metav1.Object, finalizerName string, client client.Client) DeletionState {
+ if instance.GetDeletionTimestamp().IsZero() {
+ return DeletionState{FinalizerRemoved: false, Deleted: false}
+ }
+
+ annotationValue, exists := instance.GetAnnotations()[DeleteProtectionAnnotation]
+
+ var protected bool
+ var err error
+ if exists {
+ protected, err = strconv.ParseBool(annotationValue)
+ // Assume true if it can't be parsed
+ if err != nil {
+ protected = true
+ // We need to reset the error again, so we don't trigger any unwanted side effects...
+ err = nil
+ }
+ } else {
+ protected = false
+ }
+
+ if SliceContainsString(instance.GetFinalizers(), finalizerName) && !protected {
+
+ controllerutil.RemoveFinalizer(instance, finalizerName)
+
+ return DeletionState{Deleted: true, FinalizerRemoved: true}
+ }
+
+ return DeletionState{Deleted: true, FinalizerRemoved: false}
+}
+
+func AddDeletionProtection(instance metav1.Object) {
+ config := os.Getenv(protectionSettingEnvVar)
+
+ protected, err := strconv.ParseBool(config)
+ if err != nil {
+ protected = true
+ }
+
+ if protected {
+ annotations := instance.GetAnnotations()
+
+ if annotations == nil {
+ annotations = make(map[string]string)
+ }
+
+ if _, ok := annotations[DeleteProtectionAnnotation]; !ok {
+ annotations[DeleteProtectionAnnotation] = "true"
+ }
+
+ instance.SetAnnotations(annotations)
+ }
+
+}
diff --git a/pkg/helpers/crd_test.go b/pkg/helpers/crd_test.go
index 09ac356c..fb78db95 100644
--- a/pkg/helpers/crd_test.go
+++ b/pkg/helpers/crd_test.go
@@ -2,22 +2,21 @@ package helpers
import (
"context"
+ "os"
"testing"
+ "time"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/client-go/kubernetes/scheme"
- "sigs.k8s.io/controller-runtime/pkg/client/fake"
-
+ "github.com/projectsyn/lieutenant-operator/pkg/apis"
synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/controller-runtime/pkg/client"
-
- "github.com/projectsyn/lieutenant-operator/pkg/apis"
-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/kubernetes/scheme"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func TestAddTenantLabel(t *testing.T) {
@@ -128,3 +127,137 @@ func testSetupClient(objs []runtime.Object) client.Client {
s.AddKnownTypes(synv1alpha1.SchemeGroupVersion, objs...)
return fake.NewFakeClient(objs...)
}
+
+func TestHandleDeletion(t *testing.T) {
+ type args struct {
+ instance metav1.Object
+ finalizerName string
+ }
+ tests := []struct {
+ name string
+ args args
+ want DeletionState
+ }{
+ {
+ name: "Normal deletion",
+ want: DeletionState{Deleted: true, FinalizerRemoved: true},
+ args: args{
+ instance: &synv1alpha1.Cluster{
+ ObjectMeta: metav1.ObjectMeta{
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ Finalizers: []string{
+ "test",
+ },
+ },
+ },
+ finalizerName: "test",
+ },
+ },
+ {
+ name: "Deletion protection set",
+ want: DeletionState{Deleted: true, FinalizerRemoved: false},
+ args: args{
+ instance: &synv1alpha1.Cluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ DeleteProtectionAnnotation: "true",
+ },
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ Finalizers: []string{
+ "test",
+ },
+ },
+ },
+ finalizerName: "test",
+ },
+ },
+ {
+ name: "Nonsense annotation value",
+ want: DeletionState{Deleted: true, FinalizerRemoved: false},
+ args: args{
+ instance: &synv1alpha1.Cluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ DeleteProtectionAnnotation: "trugadse",
+ },
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ Finalizers: []string{
+ "test",
+ },
+ },
+ },
+ finalizerName: "test",
+ },
+ },
+ {
+ name: "Object not deleted",
+ want: DeletionState{Deleted: false, FinalizerRemoved: false},
+ args: args{
+ instance: &synv1alpha1.Cluster{},
+ finalizerName: "test",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ client := testSetupClient([]runtime.Object{&synv1alpha1.Cluster{}})
+
+ got := HandleDeletion(tt.args.instance, tt.args.finalizerName, client)
+ if got != tt.want {
+ t.Errorf("HandleDeletion() = %v, want %v", got, tt.want)
+ }
+
+ })
+ }
+}
+
+func TestAddDeletionProtection(t *testing.T) {
+ type args struct {
+ instance metav1.Object
+ enable string
+ result string
+ }
+ tests := []struct {
+ name string
+ args args
+ }{
+ {
+ name: "Add deletion protection",
+ args: args{
+ instance: &synv1alpha1.Cluster{},
+ enable: "true",
+ result: "true",
+ },
+ },
+ {
+ name: "Don't add deletion protection",
+ args: args{
+ instance: &synv1alpha1.Cluster{},
+ enable: "false",
+ result: "",
+ },
+ },
+ {
+ name: "Invalid setting",
+ args: args{
+ instance: &synv1alpha1.Cluster{},
+ enable: "gaga",
+ result: "true",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ os.Setenv(protectionSettingEnvVar, tt.args.enable)
+
+ AddDeletionProtection(tt.args.instance)
+
+ result := tt.args.instance.GetAnnotations()[DeleteProtectionAnnotation]
+ if result != tt.args.result {
+ t.Errorf("AddDeletionProtection() value = %v, wantValue %v", result, tt.args.result)
+ }
+ })
+ }
+}
diff --git a/pkg/helpers/values.go b/pkg/helpers/values.go
new file mode 100644
index 00000000..c1c64173
--- /dev/null
+++ b/pkg/helpers/values.go
@@ -0,0 +1,21 @@
+package helpers
+
+import (
+ "os"
+
+ synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
+)
+
+const (
+ DeleteProtectionAnnotation = "syn.tools/protected-delete"
+)
+
+func GetDeletionPolicy() synv1alpha1.DeletionPolicy {
+ policy := synv1alpha1.DeletionPolicy(os.Getenv("DEFAULT_DELETION_POLICY"))
+ switch policy {
+ case synv1alpha1.ArchivePolicy, synv1alpha1.DeletePolicy, synv1alpha1.RetainPolicy:
+ return policy
+ default:
+ return synv1alpha1.ArchivePolicy
+ }
+}
diff --git a/pkg/vault/client.go b/pkg/vault/client.go
index 31f7524e..63734e8d 100644
--- a/pkg/vault/client.go
+++ b/pkg/vault/client.go
@@ -1,12 +1,16 @@
package vault
import (
+ "fmt"
"os"
"path"
+ "sort"
+ "strconv"
"github.com/banzaicloud/bank-vaults/pkg/sdk/vault"
"github.com/go-logr/logr"
"github.com/hashicorp/vault/api"
+ synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
)
const (
@@ -19,30 +23,49 @@ var (
instanceClient VaultClient
)
-type VaultClient interface {
- SetToken(secretPath string, token string, log logr.Logger) error
+// TODO: similar map like the template files
+type VaultSecret struct {
+ Path string
+ Value string
}
-type BankVaultClient struct {
- client *vault.Client
- secretEngine string
+type VaultClient interface {
+ AddSecrets(secrets []VaultSecret) error
+ // remove specific secret
+ RemoveSecrets(secret []VaultSecret) error
}
-// SetCustomClient is used if a custom client needs to be used. Currently only
-// used for testing.
-func SetCustomClient(c VaultClient) {
- instanceClient = c
+type BankVaultClient struct {
+ client *vault.Client
+ secretEngine string
+ deletionPolicy synv1alpha1.DeletionPolicy
+ log logr.Logger
}
// NewClient returns the default VaultClient implementation, ready to be used.
// It automatically detects, if there was a Vault token provided or if it's
// running withing kubernetes.
-func NewClient() (VaultClient, error) {
+func NewClient(deletionPolicy synv1alpha1.DeletionPolicy, log logr.Logger) (VaultClient, error) {
if instanceClient != nil {
return instanceClient, nil
}
+ var err error
+ instanceClient, err = newBankVaultClient(deletionPolicy, log)
+
+ return instanceClient, err
+
+}
+
+// SetCustomClient is used if a custom client needs to be used. Currently only
+// used for testing.
+func SetCustomClient(c VaultClient) {
+ instanceClient = c
+}
+
+func newBankVaultClient(deletionPolicy synv1alpha1.DeletionPolicy, log logr.Logger) (*BankVaultClient, error) {
+
client, err := vault.NewClientFromConfig(&api.Config{
Address: os.Getenv(api.EnvVaultAddress),
}, vault.ClientRole("lieutenant-operator"))
@@ -55,18 +78,29 @@ func NewClient() (VaultClient, error) {
client.RawClient().SetToken(os.Getenv(api.EnvVaultToken))
}
- instanceClient = &BankVaultClient{
- client: client,
- secretEngine: "kv",
- }
+ return &BankVaultClient{
+ client: client,
+ secretEngine: "kv",
+ deletionPolicy: deletionPolicy,
+ log: log,
+ }, nil
- return instanceClient, nil
}
-// SetToken saves the token in Vault, the path should have the form
+func (b *BankVaultClient) AddSecrets(secrets []VaultSecret) error {
+ for _, secret := range secrets {
+ err := b.addSecret(secret.Path, secret.Value)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// addSecret saves the token in Vault, the path should have the form
// tenant/cluster to work properly. It will check if the token exists and
// re-apply it if not.
-func (b *BankVaultClient) SetToken(secretPath, token string, log logr.Logger) error {
+func (b *BankVaultClient) addSecret(secretPath, token string) error {
queryPath := path.Join(b.secretEngine, "data", secretPath)
@@ -76,7 +110,7 @@ func (b *BankVaultClient) SetToken(secretPath, token string, log logr.Logger) er
}
if secret == nil {
- log.WithName("vault").Info("does not yet exist, creating", "name", secretPath)
+ b.log.WithName("vault").Info("does not yet exist, creating", "name", secretPath)
secret = &api.Secret{}
secret.Data = vault.NewData(0, map[string]interface{}{
tokenName: token,
@@ -85,16 +119,138 @@ func (b *BankVaultClient) SetToken(secretPath, token string, log logr.Logger) er
return err
}
- secretData := secret.Data["data"].(map[string]interface{})
+ secretData, ok := secret.Data["data"].(map[string]interface{})
- if secretData[tokenName] != token {
+ if !ok {
+ secretData = make(map[string]interface{})
+ }
+
+ if !ok || secretData[tokenName] != token {
- log.WithName("vault").Info("secrets don't match, re-applying")
+ b.log.WithName("vault").Info("secrets don't match, re-applying")
secretData[tokenName] = token
+ secret.Data["data"] = secretData
+
_, err = b.client.RawClient().Logical().Write(queryPath, secret.Data)
}
return err
}
+
+// RemoveSecrets will remove all the keys bellow the given paths. It will list
+// all secrets of in the path and delete them according to the deletion policy.
+func (b *BankVaultClient) RemoveSecrets(secrets []VaultSecret) error {
+ for _, secret := range secrets {
+ err := b.removeSecret(secret)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// removeSecret will remove the token according to the DeletetionPolicy
+func (b *BankVaultClient) removeSecret(removeSecret VaultSecret) error {
+
+ secrets, err := b.listSecrets(removeSecret.Path)
+ if err != nil {
+ return err
+ }
+
+ for _, secret := range secrets {
+
+ sPath := path.Join(b.secretEngine, "metadata", removeSecret.Path, secret)
+
+ s, err := b.client.RawClient().Logical().Read(sPath)
+ if err != nil {
+ return err
+ }
+
+ versions, err := b.getVersionList(s.Data)
+ if err != nil {
+ return err
+ }
+
+ switch b.deletionPolicy {
+ case synv1alpha1.ArchivePolicy:
+ b.log.Info("soft deleting secret", "secret", removeSecret.Path)
+ err := b.deleteToken(path.Join(b.secretEngine, "delete", removeSecret.Path, secret), versions)
+ if err != nil {
+ return err
+ }
+ case synv1alpha1.DeletePolicy:
+ b.log.Info("destroying secret", "secret", removeSecret.Path)
+ err := b.destroyToken(path.Join(b.secretEngine, "metadata", removeSecret.Path, secret), versions)
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unknown DeletionPolicy, skipping")
+ }
+ }
+
+ return nil
+}
+
+func (b *BankVaultClient) getVersionList(data map[string]interface{}) (map[string]interface{}, error) {
+
+ versionlist := make([]int, 0)
+
+ if versions, ok := data["versions"]; ok {
+
+ version, ok := versions.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("can't parse versions from secret")
+ }
+
+ for k := range version {
+ if v, err := strconv.Atoi(k); err == nil {
+ versionlist = append(versionlist, v)
+ } else {
+ return nil, err
+ }
+
+ }
+
+ }
+
+ sort.Ints(versionlist)
+
+ return map[string]interface{}{"versions": versionlist}, nil
+}
+
+func (b *BankVaultClient) destroyToken(secretPath string, versions map[string]interface{}) error {
+ _, err := b.client.RawClient().Logical().Delete(secretPath)
+ return err
+}
+
+func (b *BankVaultClient) deleteToken(secretPath string, versions map[string]interface{}) error {
+ _, err := b.client.RawClient().Logical().Write(secretPath, versions)
+ return err
+}
+
+func (b *BankVaultClient) listSecrets(secretPath string) ([]string, error) {
+
+ secrets, err := b.client.RawClient().Logical().List(path.Join(b.secretEngine, "metadata", secretPath))
+ if err != nil {
+ return nil, err
+ }
+ data, ok := secrets.Data["keys"].([]interface{})
+ if !ok {
+ return nil, fmt.Errorf("list of secrets can't be decoded")
+ }
+
+ result := []string{}
+ for _, secret := range data {
+ s, ok := secret.(string)
+ if !ok {
+ return nil, fmt.Errorf("list of secrets can't be decoded")
+ }
+ result = append(result, s)
+ }
+
+ return result, nil
+
+}
diff --git a/pkg/vault/client_test.go b/pkg/vault/client_test.go
index c5286dd0..4e931cb8 100644
--- a/pkg/vault/client_test.go
+++ b/pkg/vault/client_test.go
@@ -1,14 +1,18 @@
package vault
import (
+ "io"
"net/http"
"net/http/httptest"
"os"
+ "reflect"
"testing"
"github.com/go-logr/logr"
"github.com/hashicorp/vault/api"
"github.com/operator-framework/operator-sdk/pkg/log/zap"
+ synv1alpha1 "github.com/projectsyn/lieutenant-operator/pkg/apis/syn/v1alpha1"
+ "github.com/stretchr/testify/assert"
)
func testGetHTTPServer(statusCode int, body []byte) *httptest.Server {
@@ -63,7 +67,7 @@ func TestNewClient(t *testing.T) {
defer server.Close()
- _, err := NewClient()
+ _, err := NewClient(synv1alpha1.RetainPolicy, zap.Logger())
if err != nil {
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -73,11 +77,11 @@ func TestNewClient(t *testing.T) {
}
}
-func TestBankVaultClient_SetToken(t *testing.T) {
+func TestBankVaultClient_AddSecrets(t *testing.T) {
type args struct {
- secretPath string
- token string
- log logr.Logger
+ secrets []VaultSecret
+ token string
+ log logr.Logger
}
tests := []struct {
name string
@@ -89,9 +93,9 @@ func TestBankVaultClient_SetToken(t *testing.T) {
{
name: "test SetToken",
args: args{
- secretPath: "1234/6789",
- token: "test",
- log: zap.Logger(),
+ secrets: []VaultSecret{{Path: "1234/6789", Value: ""}},
+ token: "test",
+ log: zap.Logger(),
},
body: `{
"data": {
@@ -114,9 +118,9 @@ func TestBankVaultClient_SetToken(t *testing.T) {
statusCode: 404,
body: "{}",
args: args{
- secretPath: "1234/6789",
- token: "test",
- log: zap.Logger(),
+ secrets: []VaultSecret{{Path: "1234/6789", Value: ""}},
+ token: "test",
+ log: zap.Logger(),
},
},
}
@@ -130,10 +134,216 @@ func TestBankVaultClient_SetToken(t *testing.T) {
defer server.Close()
- b, _ := NewClient()
- if err := b.SetToken(tt.args.secretPath, tt.args.token, tt.args.log); (err != nil) != tt.wantErr {
+ b, _ := NewClient(synv1alpha1.ArchivePolicy, tt.args.log)
+ if err := b.AddSecrets(tt.args.secrets); (err != nil) != tt.wantErr {
t.Errorf("BankVaultClient.SetToken() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+
+func TestBankVaultClient_RemoveSecrets(t *testing.T) {
+ type args struct {
+ secrets []VaultSecret
+ policy synv1alpha1.DeletionPolicy
+ log logr.Logger
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {
+ name: "deleting",
+ wantErr: false,
+ args: args{
+ secrets: []VaultSecret{{Path: "kv2/test", Value: ""}},
+ policy: synv1alpha1.DeletePolicy,
+ log: zap.Logger(),
+ },
+ },
+ {
+ name: "archiving",
+ wantErr: false,
+ args: args{
+ secrets: []VaultSecret{{Path: "kv2/test", Value: ""}},
+ policy: synv1alpha1.ArchivePolicy,
+ log: zap.Logger(),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ instanceClient = nil
+ server := getVersionHTTPServer()
+
+ os.Setenv(api.EnvVaultToken, "myroot")
+ os.Setenv(api.EnvVaultAddress, server.URL)
+
+ defer server.Close()
+
+ b, _ := NewClient(tt.args.policy, tt.args.log)
+ if err := b.RemoveSecrets(tt.args.secrets); (err != nil) != tt.wantErr {
+ t.Errorf("BankVaultClient.SetToken() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func getVersionHTTPServer() *httptest.Server {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/v1/kv/delete/kv2/test/foo", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ mux.HandleFunc("/v1/kv/metadata/kv2/test/foo", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ versionBody := `
+ {
+ "data": {
+ "created_time": "2018-03-22T02:24:06.945319214Z",
+ "current_version": 3,
+ "max_versions": 0,
+ "oldest_version": 0,
+ "updated_time": "2018-03-22T02:36:43.986212308Z",
+ "versions": {
+ "1": {
+ "created_time": "2018-03-22T02:24:06.945319214Z",
+ "deletion_time": "",
+ "destroyed": false
+ },
+ "2": {
+ "created_time": "2018-03-22T02:36:33.954880664Z",
+ "deletion_time": "",
+ "destroyed": false
+ },
+ "3": {
+ "created_time": "2018-03-22T02:36:43.986212308Z",
+ "deletion_time": "",
+ "destroyed": false
+ }
+ }
+ }
+ }`
+ _, _ = io.WriteString(w, versionBody)
+ })
+
+ mux.HandleFunc("/v1/kv/metadata/kv2/test", func(w http.ResponseWriter, r *http.Request) {
+
+ w.WriteHeader(http.StatusOK)
+
+ if r.URL.Query().Get("list") == "true" {
+ _, _ = io.WriteString(w, `{
+ "data": {
+ "keys": ["foo", "foo/"]
+ }
+ }`)
+ return
+ }
+
+ })
+
+ return httptest.NewServer(mux)
+}
+
+func TestBankVaultClient_getVersionList(t *testing.T) {
+ type args struct {
+ data map[string]interface{}
+ }
+ tests := []struct {
+ name string
+ args args
+ want map[string]interface{}
+ wantErr bool
+ }{
+ {
+ name: "test parsing",
+ args: args{
+ data: map[string]interface{}{
+ "versions": map[string]interface{}{
+ "1": struct{}{},
+ "2": struct{}{},
+ },
+ },
+ },
+ wantErr: false,
+ want: map[string]interface{}{
+ "versions": []int{1, 2},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ b := &BankVaultClient{}
+ got, err := b.getVersionList(tt.args.data)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("BankVaultClient.getVersionList() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("BankVaultClient.getVersionList() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func getListHTTPServer() *httptest.Server {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/v1/kv/metadata/test", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, _ = io.WriteString(w, `{
+ "data": {
+ "keys": ["foo", "foo/"]
+ }
+ }`)
+ })
+
+ return httptest.NewServer(mux)
+}
+
+func TestBankVaultClient_listSecrets(t *testing.T) {
+ type args struct {
+ secretPath string
+ policy synv1alpha1.DeletionPolicy
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ wantErr bool
+ }{
+ {
+ name: "parse test",
+ wantErr: false,
+ want: []string{"foo", "foo/"},
+ args: args{
+ secretPath: "test",
+ policy: synv1alpha1.DeletePolicy,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ instanceClient = nil
+ server := getListHTTPServer()
+
+ os.Setenv(api.EnvVaultToken, "myroot")
+ os.Setenv(api.EnvVaultAddress, server.URL)
+
+ defer server.Close()
+
+ b, err := newBankVaultClient(tt.args.policy, zap.Logger())
+ assert.NoError(t, err)
+
+ got, err := b.listSecrets(tt.args.secretPath)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("BankVaultClient.listSecrets() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("BankVaultClient.listSecrets() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}