mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 15:38:06 +00:00
Add packages
- Add the packages that help the operator work as expected. - Update `go.mod` by running `go mod tidy`.
This commit is contained in:
190
pkg/kubernetessecrets/kubernetes_secrets_builder.go
Normal file
190
pkg/kubernetessecrets/kubernetes_secrets_builder.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package kubernetessecrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"reflect"
|
||||
|
||||
errs "errors"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
const OnepasswordPrefix = "operator.1password.io"
|
||||
const NameAnnotation = OnepasswordPrefix + "/item-name"
|
||||
const VersionAnnotation = OnepasswordPrefix + "/item-version"
|
||||
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
|
||||
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
|
||||
|
||||
var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable")
|
||||
|
||||
var log = logf.Log
|
||||
|
||||
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error {
|
||||
itemVersion := fmt.Sprint(item.Version)
|
||||
secretAnnotations := map[string]string{
|
||||
VersionAnnotation: itemVersion,
|
||||
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID),
|
||||
}
|
||||
|
||||
if autoRestart != "" {
|
||||
_, err := utils.StringToBool(autoRestart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
|
||||
}
|
||||
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
|
||||
}
|
||||
|
||||
// "Opaque" and "" secret types are treated the same by Kubernetes.
|
||||
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef)
|
||||
|
||||
currentSecret := &corev1.Secret{}
|
||||
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
log.Info(fmt.Sprintf("Creating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
|
||||
return kubeClient.Create(context.Background(), secret)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the secret types are being changed on the update.
|
||||
// Avoid Opaque and "" are treated as different on check.
|
||||
wantSecretType := secretType
|
||||
if wantSecretType == "" {
|
||||
wantSecretType = string(corev1.SecretTypeOpaque)
|
||||
}
|
||||
currentSecretType := string(currentSecret.Type)
|
||||
if currentSecretType == "" {
|
||||
currentSecretType = string(corev1.SecretTypeOpaque)
|
||||
}
|
||||
if currentSecretType != wantSecretType {
|
||||
return ErrCannotUpdateSecretType
|
||||
}
|
||||
|
||||
currentAnnotations := currentSecret.Annotations
|
||||
currentLabels := currentSecret.Labels
|
||||
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
|
||||
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
|
||||
currentSecret.ObjectMeta.Annotations = secretAnnotations
|
||||
currentSecret.ObjectMeta.Labels = labels
|
||||
currentSecret.Data = secret.Data
|
||||
if err := kubeClient.Update(context.Background(), currentSecret); err != nil {
|
||||
return fmt.Errorf("Kubernetes secret update failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation]))
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret {
|
||||
var ownerRefs []metav1.OwnerReference
|
||||
if ownerRef != nil {
|
||||
ownerRefs = []metav1.OwnerReference{*ownerRef}
|
||||
}
|
||||
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: formatSecretName(name),
|
||||
Namespace: namespace,
|
||||
Annotations: annotations,
|
||||
Labels: labels,
|
||||
OwnerReferences: ownerRefs,
|
||||
},
|
||||
Data: BuildKubernetesSecretData(item.Fields, item.Files),
|
||||
Type: corev1.SecretType(secretType),
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte {
|
||||
secretData := map[string][]byte{}
|
||||
for i := 0; i < len(fields); i++ {
|
||||
if fields[i].Value != "" {
|
||||
key := formatSecretDataName(fields[i].Label)
|
||||
secretData[key] = []byte(fields[i].Value)
|
||||
}
|
||||
}
|
||||
|
||||
// populate unpopulated fields from files
|
||||
for _, file := range files {
|
||||
content, err := file.Content()
|
||||
if err != nil {
|
||||
log.Error(err, "Could not load contents of file %s", file.Name)
|
||||
continue
|
||||
}
|
||||
if content != nil {
|
||||
key := file.Name
|
||||
if secretData[key] == nil {
|
||||
secretData[key] = content
|
||||
} else {
|
||||
log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
return secretData
|
||||
}
|
||||
|
||||
// formatSecretName rewrites a value to be a valid Secret name.
|
||||
//
|
||||
// The Secret meta.name and data keys must be valid DNS subdomain names
|
||||
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
|
||||
func formatSecretName(value string) string {
|
||||
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
|
||||
return value
|
||||
}
|
||||
return createValidSecretName(value)
|
||||
}
|
||||
|
||||
// formatSecretDataName rewrites a value to be a valid Secret data key.
|
||||
//
|
||||
// The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.`
|
||||
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
|
||||
func formatSecretDataName(value string) string {
|
||||
if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 {
|
||||
return value
|
||||
}
|
||||
return createValidSecretDataName(value)
|
||||
}
|
||||
|
||||
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+")
|
||||
|
||||
func createValidSecretName(value string) string {
|
||||
result := strings.ToLower(value)
|
||||
result = invalidDNS1123Chars.ReplaceAllString(result, "-")
|
||||
|
||||
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
|
||||
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
|
||||
}
|
||||
|
||||
// first and last character MUST be alphanumeric
|
||||
return strings.Trim(result, "-.")
|
||||
}
|
||||
|
||||
var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+")
|
||||
var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)")
|
||||
|
||||
func createValidSecretDataName(value string) string {
|
||||
result := invalidStartEndChars.ReplaceAllString(value, "")
|
||||
result = invalidDataChars.ReplaceAllString(result, "-")
|
||||
|
||||
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
|
||||
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
299
pkg/kubernetessecrets/kubernetes_secrets_builder_test.go
Normal file
299
pkg/kubernetessecrets/kubernetes_secrets_builder_test.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package kubernetessecrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
const restartDeploymentAnnotation = "false"
|
||||
|
||||
type k8s struct {
|
||||
clientset kubernetes.Interface
|
||||
}
|
||||
|
||||
func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
createdSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Secret was not created: %v", err)
|
||||
}
|
||||
compareFields(item.Fields, createdSecret.Data, t)
|
||||
compareAnnotationsToItem(createdSecret.Annotations, item, t)
|
||||
}
|
||||
|
||||
func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||
secretName := "test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
ownerRef := &metav1.OwnerReference{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
Name: "test-deployment",
|
||||
UID: types.UID("test-uid"),
|
||||
}
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, ownerRef)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
createdSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
|
||||
// Check owner references.
|
||||
gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences
|
||||
if len(gotOwnerRefs) != 1 {
|
||||
t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs))
|
||||
}
|
||||
|
||||
expOwnerRef := metav1.OwnerReference{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
Name: "test-deployment",
|
||||
UID: types.UID("test-uid"),
|
||||
}
|
||||
gotOwnerRef := gotOwnerRefs[0]
|
||||
if gotOwnerRef != expOwnerRef {
|
||||
t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
secretName := "test-secret-update"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Updating kubernetes secret with new item
|
||||
newItem := onepassword.Item{}
|
||||
newItem.Fields = generateFields(6)
|
||||
newItem.Version = 456
|
||||
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
updatedSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, updatedSecret)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Secret was not found: %v", err)
|
||||
}
|
||||
compareFields(newItem.Fields, updatedSecret.Data, t)
|
||||
compareAnnotationsToItem(updatedSecret.Annotations, newItem, t)
|
||||
}
|
||||
func TestBuildKubernetesSecretData(t *testing.T) {
|
||||
fields := generateFields(5)
|
||||
|
||||
secretData := BuildKubernetesSecretData(fields, nil)
|
||||
if len(secretData) != len(fields) {
|
||||
t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData))
|
||||
}
|
||||
compareFields(fields, secretData, t)
|
||||
}
|
||||
|
||||
func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
||||
annotationKey := "annotationKey"
|
||||
annotationValue := "annotationValue"
|
||||
name := "someName"
|
||||
namespace := "someNamespace"
|
||||
annotations := map[string]string{
|
||||
annotationKey: annotationValue,
|
||||
}
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
labels := map[string]string{}
|
||||
secretType := ""
|
||||
|
||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
|
||||
if kubeSecret.Name != strings.ToLower(name) {
|
||||
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
|
||||
}
|
||||
if kubeSecret.Namespace != namespace {
|
||||
t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace)
|
||||
}
|
||||
if kubeSecret.Annotations[annotationKey] != annotations[annotationKey] {
|
||||
t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace)
|
||||
}
|
||||
compareFields(item.Fields, kubeSecret.Data, t)
|
||||
}
|
||||
|
||||
func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
||||
name := "inV@l1d k8s secret%name"
|
||||
expectedName := "inv-l1d-k8s-secret-name"
|
||||
namespace := "someNamespace"
|
||||
annotations := map[string]string{
|
||||
"annotationKey": "annotationValue",
|
||||
}
|
||||
labels := map[string]string{}
|
||||
item := onepassword.Item{}
|
||||
secretType := ""
|
||||
|
||||
item.Fields = []*onepassword.ItemField{
|
||||
{
|
||||
Label: "label w%th invalid ch!rs-",
|
||||
Value: "value1",
|
||||
},
|
||||
{
|
||||
Label: strings.Repeat("x", kubeValidate.DNS1123SubdomainMaxLength+1),
|
||||
Value: "name exceeds max length",
|
||||
},
|
||||
}
|
||||
|
||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
|
||||
|
||||
// Assert Secret's meta.name was fixed
|
||||
if kubeSecret.Name != expectedName {
|
||||
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
|
||||
}
|
||||
if kubeSecret.Namespace != namespace {
|
||||
t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace)
|
||||
}
|
||||
|
||||
// assert labels were fixed for each data key
|
||||
for key := range kubeSecret.Data {
|
||||
if !validLabel(key) {
|
||||
t.Errorf("Expected valid kubernetes label, got %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
|
||||
secretName := "tls-test-secret-name"
|
||||
namespace := "test"
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(5)
|
||||
item.Version = 123
|
||||
item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
|
||||
|
||||
kubeClient := fake.NewFakeClient()
|
||||
secretLabels := map[string]string{}
|
||||
secretType := "kubernetes.io/tls"
|
||||
|
||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
createdSecret := &corev1.Secret{}
|
||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Secret was not created: %v", err)
|
||||
}
|
||||
|
||||
if createdSecret.Type != corev1.SecretTypeTLS {
|
||||
t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type))
|
||||
}
|
||||
}
|
||||
|
||||
func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) {
|
||||
actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
|
||||
if err != nil {
|
||||
t.Errorf("Was unable to parse Item Path")
|
||||
}
|
||||
if actualVaultId != item.Vault.ID {
|
||||
t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId)
|
||||
}
|
||||
if actualItemId != item.ID {
|
||||
t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId)
|
||||
}
|
||||
if annotations[VersionAnnotation] != fmt.Sprint(item.Version) {
|
||||
t.Errorf("Expected annotation version to be %v but was %v", item.Version, annotations[VersionAnnotation])
|
||||
}
|
||||
|
||||
if annotations[RestartDeploymentsAnnotation] != "false" {
|
||||
t.Errorf("Expected restart deployments annotation to be %v but was %v", restartDeploymentAnnotation, RestartDeploymentsAnnotation)
|
||||
}
|
||||
}
|
||||
|
||||
func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) {
|
||||
for i := 0; i < len(actualFields); i++ {
|
||||
value, found := secretData[actualFields[i].Label]
|
||||
if !found {
|
||||
t.Errorf("Expected key %v is missing from secret data", actualFields[i].Label)
|
||||
}
|
||||
if string(value) != actualFields[i].Value {
|
||||
t.Errorf("Expected value %v but got %v", actualFields[i].Value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateFields(numToGenerate int) []*onepassword.ItemField {
|
||||
fields := []*onepassword.ItemField{}
|
||||
for i := 0; i < numToGenerate; i++ {
|
||||
field := onepassword.ItemField{
|
||||
Label: "key" + fmt.Sprint(i),
|
||||
Value: "value" + fmt.Sprint(i),
|
||||
}
|
||||
fields = append(fields, &field)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
|
||||
splitPath := strings.Split(path, "/")
|
||||
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
|
||||
return splitPath[1], splitPath[3], nil
|
||||
}
|
||||
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
|
||||
}
|
||||
|
||||
func validLabel(v string) bool {
|
||||
if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
84
pkg/mocks/mocksecretserver.go
Normal file
84
pkg/mocks/mocksecretserver.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
)
|
||||
|
||||
type TestClient struct {
|
||||
GetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
GetVaultFunc func(uuid string) (*onepassword.Vault, error)
|
||||
GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
|
||||
GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
|
||||
GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
|
||||
CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
|
||||
GetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error)
|
||||
GetFileContentFunc func(file *onepassword.File) ([]byte, error)
|
||||
}
|
||||
|
||||
var (
|
||||
GetGetVaultsFunc func() ([]onepassword.Vault, error)
|
||||
DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error)
|
||||
DoGetVaultFunc func(uuid string) (*onepassword.Vault, error)
|
||||
GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error)
|
||||
DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error)
|
||||
DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error)
|
||||
DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error
|
||||
DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error)
|
||||
DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error)
|
||||
DoGetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error)
|
||||
DoGetFileContentFunc func(file *onepassword.File) ([]byte, error)
|
||||
)
|
||||
|
||||
// Do is the mock client's `Do` func
|
||||
func (m *TestClient) GetVaults() ([]onepassword.Vault, error) {
|
||||
return GetGetVaultsFunc()
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) {
|
||||
return DoGetVaultsByTitleFunc(title)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetVault(uuid string) (*onepassword.Vault, error) {
|
||||
return DoGetVaultFunc(uuid)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
return GetGetItemFunc(uuid, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsFunc(vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItemsByTitle(title, vaultUUID string) ([]onepassword.Item, error) {
|
||||
return DoGetItemsByTitleFunc(title, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoGetItemByTitleFunc(title, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoCreateItemFunc(item, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error {
|
||||
return DoDeleteItemFunc(item, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
|
||||
return DoUpdateItemFunc(item, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) {
|
||||
return DoGetFileFunc(uuid, itemUUID, vaultUUID)
|
||||
}
|
||||
|
||||
func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) {
|
||||
return DoGetFileContentFunc(file)
|
||||
}
|
60
pkg/onepassword/annotations.go
Normal file
60
pkg/onepassword/annotations.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
OnepasswordPrefix = "operator.1password.io"
|
||||
ItemPathAnnotation = OnepasswordPrefix + "/item-path"
|
||||
NameAnnotation = OnepasswordPrefix + "/item-name"
|
||||
VersionAnnotation = OnepasswordPrefix + "/item-version"
|
||||
RestartAnnotation = OnepasswordPrefix + "/last-restarted"
|
||||
RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
|
||||
)
|
||||
|
||||
func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) {
|
||||
annotationsFound := false
|
||||
annotations := FilterAnnotations(deployment.Annotations, regex)
|
||||
if len(annotations) > 0 {
|
||||
annotationsFound = true
|
||||
} else {
|
||||
annotations = FilterAnnotations(deployment.Spec.Template.Annotations, regex)
|
||||
if len(annotations) > 0 {
|
||||
annotationsFound = true
|
||||
} else {
|
||||
annotationsFound = false
|
||||
}
|
||||
}
|
||||
|
||||
return annotations, annotationsFound
|
||||
}
|
||||
|
||||
func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[string]string {
|
||||
filteredAnnotations := make(map[string]string)
|
||||
for key, value := range annotations {
|
||||
if regex.MatchString(key) && key != RestartAnnotation && key != RestartDeploymentsAnnotation {
|
||||
filteredAnnotations[key] = value
|
||||
}
|
||||
}
|
||||
return filteredAnnotations
|
||||
}
|
||||
|
||||
func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]*corev1.Secret) bool {
|
||||
_, ok := secrets[annotations[NameAnnotation]]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AppendAnnotationUpdatedSecret(annotations map[string]string, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret {
|
||||
secret, ok := secrets[annotations[NameAnnotation]]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
}
|
||||
return updatedDeploymentSecrets
|
||||
}
|
93
pkg/onepassword/annotations_test.go
Normal file
93
pkg/onepassword/annotations_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
)
|
||||
|
||||
const AnnotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+"
|
||||
|
||||
func TestFilterAnnotations(t *testing.T) {
|
||||
invalidAnnotation1 := "onepasswordconnect/vaultId"
|
||||
invalidAnnotation2 := "onepasswordconnectkubernetesSecrets"
|
||||
|
||||
annotations := getValidAnnotations()
|
||||
annotations[invalidAnnotation1] = "This should be filtered"
|
||||
annotations[invalidAnnotation2] = "This should be filtered too"
|
||||
|
||||
r, _ := regexp.Compile(AnnotationRegExpString)
|
||||
filteredAnnotations := FilterAnnotations(annotations, r)
|
||||
if len(filteredAnnotations) != 2 {
|
||||
t.Errorf("Unexpected number of filtered annotations returned. Expected 2, got %v", len(filteredAnnotations))
|
||||
}
|
||||
_, found := filteredAnnotations[ItemPathAnnotation]
|
||||
if !found {
|
||||
t.Errorf("One Password Annotation was filtered when it should not have been")
|
||||
}
|
||||
_, found = filteredAnnotations[NameAnnotation]
|
||||
if !found {
|
||||
t.Errorf("One Password Annotation was filtered when it should not have been")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTopLevelAnnotationsForDeployment(t *testing.T) {
|
||||
annotations := getValidAnnotations()
|
||||
expectedNumAnnotations := len(annotations)
|
||||
r, _ := regexp.Compile(AnnotationRegExpString)
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
deployment.Annotations = annotations
|
||||
filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r)
|
||||
|
||||
if !annotationsFound {
|
||||
t.Errorf("No annotations marked as found")
|
||||
}
|
||||
|
||||
numAnnotations := len(filteredAnnotations)
|
||||
if expectedNumAnnotations != numAnnotations {
|
||||
t.Errorf("Expected %v annotations got %v", expectedNumAnnotations, numAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTemplateAnnotationsForDeployment(t *testing.T) {
|
||||
annotations := getValidAnnotations()
|
||||
expectedNumAnnotations := len(annotations)
|
||||
r, _ := regexp.Compile(AnnotationRegExpString)
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
deployment.Spec.Template.Annotations = annotations
|
||||
filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r)
|
||||
|
||||
if !annotationsFound {
|
||||
t.Errorf("No annotations marked as found")
|
||||
}
|
||||
|
||||
numAnnotations := len(filteredAnnotations)
|
||||
if expectedNumAnnotations != numAnnotations {
|
||||
t.Errorf("Expected %v annotations got %v", expectedNumAnnotations, numAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNoAnnotationsForDeployment(t *testing.T) {
|
||||
deployment := &appsv1.Deployment{}
|
||||
r, _ := regexp.Compile(AnnotationRegExpString)
|
||||
filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r)
|
||||
|
||||
if annotationsFound {
|
||||
t.Errorf("No annotations should be found")
|
||||
}
|
||||
|
||||
numAnnotations := len(filteredAnnotations)
|
||||
if 0 != numAnnotations {
|
||||
t.Errorf("Expected %v annotations got %v", 0, numAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
func getValidAnnotations() map[string]string {
|
||||
return map[string]string{
|
||||
ItemPathAnnotation: "vaults/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f/items/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f",
|
||||
NameAnnotation: "secretName",
|
||||
}
|
||||
}
|
117
pkg/onepassword/connect_setup.go
Normal file
117
pkg/onepassword/connect_setup.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"context"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"os"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
var logConnectSetup = logf.Log.WithName("ConnectSetup")
|
||||
var deploymentPath = "deploy/connect/deployment.yaml"
|
||||
var servicePath = "deploy/connect/service.yaml"
|
||||
|
||||
func SetupConnect(kubeClient client.Client, deploymentNamespace string) error {
|
||||
err := setupService(kubeClient, servicePath, deploymentNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error {
|
||||
existingDeployment := &appsv1.Deployment{}
|
||||
|
||||
// check if deployment has already been created
|
||||
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logConnectSetup.Info("No existing Connect deployment found. Creating Deployment")
|
||||
return createDeployment(kubeClient, deploymentPath, deploymentNamespace)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error {
|
||||
deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = kubeClient.Create(context.Background(), deployment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) {
|
||||
f, err := os.Open(deploymentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deployment := &appsv1.Deployment{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: deploymentNamespace,
|
||||
},
|
||||
}
|
||||
|
||||
err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return deployment, nil
|
||||
}
|
||||
|
||||
func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error {
|
||||
existingService := &corev1.Service{}
|
||||
|
||||
//check if service has already been created
|
||||
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logConnectSetup.Info("No existing Connect service found. Creating Service")
|
||||
return createService(kubeClient, servicePath, deploymentNamespace)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error {
|
||||
f, err := os.Open(servicePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := &corev1.Service{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: deploymentNamespace,
|
||||
},
|
||||
}
|
||||
|
||||
err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = kubeClient.Create(context.Background(), service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
65
pkg/onepassword/connect_setup_test.go
Normal file
65
pkg/onepassword/connect_setup_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var defaultNamespacedName = types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}
|
||||
|
||||
func TestServiceSetup(t *testing.T) {
|
||||
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
|
||||
// Objects to track in the fake client.
|
||||
objs := []runtime.Object{}
|
||||
|
||||
// Create a fake client to mock API calls.
|
||||
client := fake.NewFakeClientWithScheme(s, objs...)
|
||||
|
||||
err := setupService(client, "../../deploy/connect/service.yaml", defaultNamespacedName.Namespace)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect: %v", err)
|
||||
}
|
||||
|
||||
// check that service was created
|
||||
service := &corev1.Service{}
|
||||
err = client.Get(context.TODO(), defaultNamespacedName, service)
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect service: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeploymentSetup(t *testing.T) {
|
||||
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
|
||||
// Objects to track in the fake client.
|
||||
objs := []runtime.Object{}
|
||||
|
||||
// Create a fake client to mock API calls.
|
||||
client := fake.NewFakeClientWithScheme(s, objs...)
|
||||
|
||||
err := setupDeployment(client, "../../deploy/connect/deployment.yaml", defaultNamespacedName.Namespace)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect: %v", err)
|
||||
}
|
||||
|
||||
// check that deployment was created
|
||||
deployment := &appsv1.Deployment{}
|
||||
err = client.Get(context.TODO(), defaultNamespacedName, deployment)
|
||||
if err != nil {
|
||||
t.Errorf("Error Setting Up Connect deployment: %v", err)
|
||||
}
|
||||
}
|
53
pkg/onepassword/containers.go
Normal file
53
pkg/onepassword/containers.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool {
|
||||
for i := 0; i < len(containers); i++ {
|
||||
envVariables := containers[i].Env
|
||||
for j := 0; j < len(envVariables); j++ {
|
||||
if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil {
|
||||
_, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
envFromVariables := containers[i].EnvFrom
|
||||
for j := 0; j < len(envFromVariables); j++ {
|
||||
if envFromVariables[j].SecretRef != nil {
|
||||
_, ok := secrets[envFromVariables[j].SecretRef.Name]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret {
|
||||
for i := 0; i < len(containers); i++ {
|
||||
envVariables := containers[i].Env
|
||||
for j := 0; j < len(envVariables); j++ {
|
||||
if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil {
|
||||
secret, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
envFromVariables := containers[i].EnvFrom
|
||||
for j := 0; j < len(envFromVariables); j++ {
|
||||
if envFromVariables[j].SecretRef != nil {
|
||||
secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return updatedDeploymentSecrets
|
||||
}
|
85
pkg/onepassword/containers_test.go
Normal file
85
pkg/onepassword/containers_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAreContainersUsingSecretsFromEnv(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
containers := generateContainersWithSecretRefsFromEnv(containerSecretNames)
|
||||
|
||||
if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
|
||||
t.Errorf("Expected that containers were using secrets but they were not detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreContainersUsingSecretsFromEnvFrom(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames)
|
||||
|
||||
if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
|
||||
t.Errorf("Expected that containers were using secrets but they were not detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreContainersNotUsingSecrets(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
containers := generateContainersWithSecretRefsFromEnv(containerSecretNames)
|
||||
|
||||
if AreContainersUsingSecrets(containers, secretNamesToSearch) {
|
||||
t.Errorf("Expected that containers were not using secrets but they were detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": {},
|
||||
"onepassword-api-key": {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
"onepassword-api-key",
|
||||
}
|
||||
|
||||
containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames)
|
||||
|
||||
updatedDeploymentSecrets := map[string]*corev1.Secret{}
|
||||
updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets)
|
||||
|
||||
secretKeyName := "onepassword-api-key"
|
||||
|
||||
if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] {
|
||||
t.Errorf("Expected that updated Secret from envfrom is found.")
|
||||
}
|
||||
}
|
26
pkg/onepassword/deployments.go
Normal file
26
pkg/onepassword/deployments.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func IsDeploymentUsingSecrets(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) bool {
|
||||
volumes := deployment.Spec.Template.Spec.Volumes
|
||||
containers := deployment.Spec.Template.Spec.Containers
|
||||
containers = append(containers, deployment.Spec.Template.Spec.InitContainers...)
|
||||
return AreAnnotationsUsingSecrets(deployment.Annotations, secrets) || AreContainersUsingSecrets(containers, secrets) || AreVolumesUsingSecrets(volumes, secrets)
|
||||
}
|
||||
|
||||
func GetUpdatedSecretsForDeployment(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) map[string]*corev1.Secret {
|
||||
volumes := deployment.Spec.Template.Spec.Volumes
|
||||
containers := deployment.Spec.Template.Spec.Containers
|
||||
containers = append(containers, deployment.Spec.Template.Spec.InitContainers...)
|
||||
|
||||
updatedSecretsForDeployment := map[string]*corev1.Secret{}
|
||||
AppendAnnotationUpdatedSecret(deployment.Annotations, secrets, updatedSecretsForDeployment)
|
||||
AppendUpdatedContainerSecrets(containers, secrets, updatedSecretsForDeployment)
|
||||
AppendUpdatedVolumeSecrets(volumes, secrets, updatedSecretsForDeployment)
|
||||
|
||||
return updatedSecretsForDeployment
|
||||
}
|
58
pkg/onepassword/deployments_test.go
Normal file
58
pkg/onepassword/deployments_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
deployment.Spec.Template.Spec.Volumes = generateVolumes(volumeSecretNames)
|
||||
if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) {
|
||||
t.Errorf("Expected that deployment was using secrets but they were not detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
}
|
||||
|
||||
containerSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames)
|
||||
if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) {
|
||||
t.Errorf("Expected that deployment was using secrets but they were not detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDeploymentNotUSingSecrets(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
}
|
||||
|
||||
deployment := &appsv1.Deployment{}
|
||||
if IsDeploymentUsingSecrets(deployment, secretNamesToSearch) {
|
||||
t.Errorf("Expected that deployment was using not secrets but they were detected.")
|
||||
}
|
||||
}
|
100
pkg/onepassword/items.go
Normal file
100
pkg/onepassword/items.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
var logger = logf.Log.WithName("retrieve_item")
|
||||
|
||||
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
|
||||
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vaultId, err := getVaultId(opConnectClient, vaultValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
itemId, err := getItemId(opConnectClient, itemValue, vaultId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item, err := opConnectClient.GetItem(itemId, vaultId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range item.Files {
|
||||
_, err := opConnectClient.GetFileContent(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func ParseVaultAndItemFromPath(path string) (string, string, error) {
|
||||
splitPath := strings.Split(path, "/")
|
||||
if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
|
||||
return splitPath[1], splitPath[3], nil
|
||||
}
|
||||
return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
|
||||
}
|
||||
|
||||
func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {
|
||||
if !IsValidClientUUID(vaultIdentifier) {
|
||||
vaults, err := client.GetVaultsByTitle(vaultIdentifier)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(vaults) == 0 {
|
||||
return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier)
|
||||
}
|
||||
|
||||
oldestVault := vaults[0]
|
||||
if len(vaults) > 1 {
|
||||
for _, returnedVault := range vaults {
|
||||
if returnedVault.CreatedAt.Before(oldestVault.CreatedAt) {
|
||||
oldestVault = returnedVault
|
||||
}
|
||||
}
|
||||
logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID))
|
||||
}
|
||||
vaultIdentifier = oldestVault.ID
|
||||
}
|
||||
return vaultIdentifier, nil
|
||||
}
|
||||
|
||||
func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) {
|
||||
if !IsValidClientUUID(itemIdentifier) {
|
||||
items, err := client.GetItemsByTitle(itemIdentifier, vaultId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return "", fmt.Errorf("No items found with identifier %q", itemIdentifier)
|
||||
}
|
||||
|
||||
oldestItem := items[0]
|
||||
if len(items) > 1 {
|
||||
for _, returnedItem := range items {
|
||||
if returnedItem.CreatedAt.Before(oldestItem.CreatedAt) {
|
||||
oldestItem = returnedItem
|
||||
}
|
||||
}
|
||||
logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemIdentifier, oldestItem.ID))
|
||||
}
|
||||
itemIdentifier = oldestItem.ID
|
||||
}
|
||||
return itemIdentifier, nil
|
||||
}
|
54
pkg/onepassword/object_generators_for_test.go
Normal file
54
pkg/onepassword/object_generators_for_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package onepassword
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
func generateVolumes(names []string) []corev1.Volume {
|
||||
volumes := []corev1.Volume{}
|
||||
for i := 0; i < len(names); i++ {
|
||||
volume := corev1.Volume{
|
||||
Name: names[i],
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: names[i],
|
||||
},
|
||||
},
|
||||
}
|
||||
volumes = append(volumes, volume)
|
||||
}
|
||||
return volumes
|
||||
}
|
||||
func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container {
|
||||
containers := []corev1.Container{}
|
||||
for i := 0; i < len(names); i++ {
|
||||
container := corev1.Container{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "someName",
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: names[i],
|
||||
},
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
containers = append(containers, container)
|
||||
}
|
||||
return containers
|
||||
}
|
||||
|
||||
func generateContainersWithSecretRefsFromEnvFrom(names []string) []corev1.Container {
|
||||
containers := []corev1.Container{}
|
||||
for i := 0; i < len(names); i++ {
|
||||
container := corev1.Container{
|
||||
EnvFrom: []corev1.EnvFromSource{
|
||||
{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: names[i]}}},
|
||||
},
|
||||
}
|
||||
containers = append(containers, container)
|
||||
}
|
||||
return containers
|
||||
}
|
254
pkg/onepassword/secret_update_handler.go
Normal file
254
pkg/onepassword/secret_update_handler.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
|
||||
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/connect"
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
const envHostVariable = "OP_HOST"
|
||||
const lockTag = "operator.1password.io:ignore-secret"
|
||||
|
||||
var log = logf.Log.WithName("update_op_kubernetes_secrets_task")
|
||||
|
||||
func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler {
|
||||
return &SecretUpdateHandler{
|
||||
client: kubernetesClient,
|
||||
opConnectClient: opConnectClient,
|
||||
shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal,
|
||||
}
|
||||
}
|
||||
|
||||
type SecretUpdateHandler struct {
|
||||
client client.Client
|
||||
opConnectClient connect.Client
|
||||
shouldAutoRestartDeploymentsGlobal bool
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error {
|
||||
updatedKubernetesSecrets, err := h.updateKubernetesSecrets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets)
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret) error {
|
||||
// No secrets to update. Exit
|
||||
if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
deployments := &appsv1.DeploymentList{}
|
||||
err := h.client.List(context.Background(), deployments)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to list kubernetes deployments")
|
||||
return err
|
||||
}
|
||||
|
||||
if len(deployments.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < len(deployments.Items); i++ {
|
||||
deployment := &deployments.Items[i]
|
||||
updatedSecrets := updatedSecretsByNamespace[deployment.Namespace]
|
||||
|
||||
updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets)
|
||||
if len(updatedDeploymentSecrets) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, secret := range updatedDeploymentSecrets {
|
||||
if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) {
|
||||
h.restartDeployment(deployment)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace))
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) {
|
||||
log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
|
||||
if deployment.Spec.Template.Annotations == nil {
|
||||
deployment.Spec.Template.Annotations = map[string]string{}
|
||||
}
|
||||
deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
|
||||
err := h.client.Update(context.Background(), deployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Problem restarting deployment")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*corev1.Secret, error) {
|
||||
secrets := &corev1.SecretList{}
|
||||
err := h.client.List(context.Background(), secrets)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to list kubernetes secrets")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedSecrets := map[string]map[string]*corev1.Secret{}
|
||||
for i := 0; i < len(secrets.Items); i++ {
|
||||
secret := secrets.Items[i]
|
||||
|
||||
itemPath := secret.Annotations[ItemPathAnnotation]
|
||||
currentVersion := secret.Annotations[VersionAnnotation]
|
||||
if len(itemPath) == 0 || len(currentVersion) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
|
||||
|
||||
item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath)
|
||||
if err != nil {
|
||||
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
itemVersion := fmt.Sprint(item.Version)
|
||||
itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
|
||||
|
||||
if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString {
|
||||
if isItemLockedForForcedRestarts(item) {
|
||||
log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName()))
|
||||
secret.Annotations[VersionAnnotation] = itemVersion
|
||||
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s annotations to version %d: %s", secret.Name, itemVersion, err)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
|
||||
secret.Annotations[VersionAnnotation] = itemVersion
|
||||
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||
secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files)
|
||||
log.Info(fmt.Sprintf("New secret path: %v and version: %v", secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation]))
|
||||
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||
log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err)
|
||||
continue
|
||||
}
|
||||
if updatedSecrets[secret.Namespace] == nil {
|
||||
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)
|
||||
}
|
||||
updatedSecrets[secret.Namespace][secret.Name] = &secret
|
||||
}
|
||||
}
|
||||
return updatedSecrets, nil
|
||||
}
|
||||
|
||||
func isItemLockedForForcedRestarts(item *onepassword.Item) bool {
|
||||
tags := item.Tags
|
||||
for i := 0; i < len(tags); i++ {
|
||||
if tags[i] == lockTag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret) bool {
|
||||
_, ok := updatedSecrets[secretName]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string]bool, error) {
|
||||
namespaces := &corev1.NamespaceList{}
|
||||
err := h.client.List(context.Background(), namespaces)
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to list kubernetes namespaces")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespacesMap := map[string]bool{}
|
||||
|
||||
for _, namespace := range namespaces.Items {
|
||||
namespacesMap[namespace.Name] = h.isNamespaceSetToAutoRestart(&namespace)
|
||||
}
|
||||
return namespacesMap, nil
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string {
|
||||
onePasswordItem := &onepasswordv1.OnePasswordItem{}
|
||||
|
||||
// Search for our original OnePasswordItem if it exists
|
||||
err := h.client.Get(context.TODO(), client.ObjectKey{
|
||||
Namespace: secret.Namespace,
|
||||
Name: secret.Name}, onePasswordItem)
|
||||
|
||||
if err == nil {
|
||||
return onePasswordItem.Spec.ItemPath
|
||||
}
|
||||
|
||||
// If we can't find the OnePassword Item we'll just return the annotation from the secret item.
|
||||
return secret.Annotations[ItemPathAnnotation]
|
||||
}
|
||||
|
||||
func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
|
||||
restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
|
||||
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
|
||||
if restartDeployment == "" {
|
||||
return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace)
|
||||
}
|
||||
|
||||
restartDeploymentBool, err := utils.StringToBool(restartDeployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secret.Name)
|
||||
return false
|
||||
}
|
||||
return restartDeploymentBool
|
||||
}
|
||||
|
||||
func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
|
||||
restartDeployment := deployment.Annotations[RestartDeploymentsAnnotation]
|
||||
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
|
||||
if restartDeployment == "" {
|
||||
return setForAutoRestartByNamespace[deployment.Namespace]
|
||||
}
|
||||
|
||||
restartDeploymentBool, err := utils.StringToBool(restartDeployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Error parsing %v annotation on Deployment %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, deployment.Name)
|
||||
return false
|
||||
}
|
||||
return restartDeploymentBool
|
||||
}
|
||||
|
||||
func (h *SecretUpdateHandler) isNamespaceSetToAutoRestart(namespace *corev1.Namespace) bool {
|
||||
restartDeployment := namespace.Annotations[RestartDeploymentsAnnotation]
|
||||
//If annotation for auto restarts for deployment is not set. Check environment variable set on the operator
|
||||
if restartDeployment == "" {
|
||||
return h.shouldAutoRestartDeploymentsGlobal
|
||||
}
|
||||
|
||||
restartDeploymentBool, err := utils.StringToBool(restartDeployment)
|
||||
if err != nil {
|
||||
log.Error(err, "Error parsing %v annotation on Namespace %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, namespace.Name)
|
||||
return false
|
||||
}
|
||||
return restartDeploymentBool
|
||||
}
|
894
pkg/onepassword/secret_update_handler_test.go
Normal file
894
pkg/onepassword/secret_update_handler_test.go
Normal file
@@ -0,0 +1,894 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/1Password/onepassword-operator/pkg/mocks"
|
||||
|
||||
"github.com/1Password/connect-sdk-go/onepassword"
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
errors2 "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentKind = "Deployment"
|
||||
deploymentAPIVersion = "v1"
|
||||
name = "test-deployment"
|
||||
namespace = "default"
|
||||
vaultId = "hfnjvi6aymbsnfc2xeeoheizda"
|
||||
itemId = "nwrhuano7bcwddcviubpp4mhfq"
|
||||
username = "test-user"
|
||||
password = "QmHumKc$mUeEem7caHtbaBaJ"
|
||||
userKey = "username"
|
||||
passKey = "password"
|
||||
itemVersion = 123
|
||||
)
|
||||
|
||||
type testUpdateSecretTask struct {
|
||||
testName string
|
||||
existingDeployment *appsv1.Deployment
|
||||
existingNamespace *corev1.Namespace
|
||||
existingSecret *corev1.Secret
|
||||
expectedError error
|
||||
expectedResultSecret *corev1.Secret
|
||||
expectedEvents []string
|
||||
opItem map[string]string
|
||||
expectedRestart bool
|
||||
globalAutoRestartEnabled bool
|
||||
}
|
||||
|
||||
var (
|
||||
expectedSecretData = map[string][]byte{
|
||||
"password": []byte(password),
|
||||
"username": []byte(username),
|
||||
}
|
||||
itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
|
||||
)
|
||||
|
||||
var defaultNamespace = &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
|
||||
var tests = []testUpdateSecretTask{
|
||||
{
|
||||
testName: "Test unrelated deployment is not restarted with an updated secret",
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
NameAnnotation: "unlrelated secret",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: false,
|
||||
globalAutoRestartEnabled: true,
|
||||
},
|
||||
{
|
||||
testName: "OP item has new version. Secret needs update. Deployment is restarted based on containers",
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: true,
|
||||
globalAutoRestartEnabled: true,
|
||||
},
|
||||
{
|
||||
testName: "OP item has new version. Secret needs update. Deployment is restarted based on annotation",
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
ItemPathAnnotation: itemPath,
|
||||
NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: true,
|
||||
globalAutoRestartEnabled: true,
|
||||
},
|
||||
{
|
||||
testName: "OP item has new version. Secret needs update. Deployment is restarted based on volume",
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: name,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: true,
|
||||
globalAutoRestartEnabled: true,
|
||||
},
|
||||
{
|
||||
testName: "No secrets need update. No deployment is restarted",
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
ItemPathAnnotation: itemPath,
|
||||
NameAnnotation: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: false,
|
||||
globalAutoRestartEnabled: true,
|
||||
},
|
||||
{
|
||||
testName: `Deployment is not restarted when no auto restart is set to true for all
|
||||
deployments and is not overwritten by by a namespace or deployment annotation`,
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: false,
|
||||
globalAutoRestartEnabled: false,
|
||||
},
|
||||
{
|
||||
testName: `Secret autostart true value takes precedence over false deployment value`,
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
RestartDeploymentsAnnotation: "false",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
RestartDeploymentsAnnotation: "true",
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
RestartDeploymentsAnnotation: "true",
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: true,
|
||||
globalAutoRestartEnabled: false,
|
||||
},
|
||||
{
|
||||
testName: `Secret autostart true value takes precedence over false deployment value`,
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
RestartDeploymentsAnnotation: "true",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
RestartDeploymentsAnnotation: "false",
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
RestartDeploymentsAnnotation: "false",
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: false,
|
||||
globalAutoRestartEnabled: true,
|
||||
},
|
||||
{
|
||||
testName: `Deployment autostart true value takes precedence over false global auto restart value`,
|
||||
existingNamespace: defaultNamespace,
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
RestartDeploymentsAnnotation: "true",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: true,
|
||||
globalAutoRestartEnabled: false,
|
||||
},
|
||||
{
|
||||
testName: `Deployment autostart false value takes precedence over false global auto restart value,
|
||||
and true namespace value.`,
|
||||
existingNamespace: &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
Annotations: map[string]string{
|
||||
RestartDeploymentsAnnotation: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
RestartDeploymentsAnnotation: "false",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: false,
|
||||
globalAutoRestartEnabled: false,
|
||||
},
|
||||
{
|
||||
testName: `Namespace autostart true value takes precedence over false global auto restart value`,
|
||||
existingNamespace: &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
Annotations: map[string]string{
|
||||
RestartDeploymentsAnnotation: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
existingDeployment: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: deploymentKind,
|
||||
APIVersion: deploymentAPIVersion,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: name,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Key: passKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: "old version",
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
expectedError: nil,
|
||||
expectedResultSecret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
VersionAnnotation: fmt.Sprint(itemVersion),
|
||||
ItemPathAnnotation: itemPath,
|
||||
},
|
||||
},
|
||||
Data: expectedSecretData,
|
||||
},
|
||||
opItem: map[string]string{
|
||||
userKey: username,
|
||||
passKey: password,
|
||||
},
|
||||
expectedRestart: true,
|
||||
globalAutoRestartEnabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestUpdateSecretHandler(t *testing.T) {
|
||||
for _, testData := range tests {
|
||||
t.Run(testData.testName, func(t *testing.T) {
|
||||
|
||||
// Register operator types with the runtime scheme.
|
||||
s := scheme.Scheme
|
||||
s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.existingDeployment)
|
||||
|
||||
// Objects to track in the fake client.
|
||||
objs := []runtime.Object{
|
||||
testData.existingDeployment,
|
||||
testData.existingNamespace,
|
||||
}
|
||||
|
||||
if testData.existingSecret != nil {
|
||||
objs = append(objs, testData.existingSecret)
|
||||
}
|
||||
|
||||
// Create a fake client to mock API calls.
|
||||
cl := fake.NewFakeClientWithScheme(s, objs...)
|
||||
|
||||
opConnectClient := &mocks.TestClient{}
|
||||
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
|
||||
|
||||
item := onepassword.Item{}
|
||||
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"])
|
||||
item.Version = itemVersion
|
||||
item.Vault.ID = vaultUUID
|
||||
item.ID = uuid
|
||||
return &item, nil
|
||||
}
|
||||
h := &SecretUpdateHandler{
|
||||
client: cl,
|
||||
opConnectClient: opConnectClient,
|
||||
shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled,
|
||||
}
|
||||
|
||||
err := h.UpdateKubernetesSecretsTask()
|
||||
|
||||
assert.Equal(t, testData.expectedError, err)
|
||||
|
||||
var expectedSecretName string
|
||||
if testData.expectedResultSecret == nil {
|
||||
expectedSecretName = testData.existingDeployment.Name
|
||||
} else {
|
||||
expectedSecretName = testData.expectedResultSecret.Name
|
||||
}
|
||||
|
||||
// Check if Secret has been created and has the correct data
|
||||
secret := &corev1.Secret{}
|
||||
err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret)
|
||||
|
||||
if testData.expectedResultSecret == nil {
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors2.IsNotFound(err))
|
||||
} else {
|
||||
assert.Equal(t, testData.expectedResultSecret.Data, secret.Data)
|
||||
assert.Equal(t, testData.expectedResultSecret.Name, secret.Name)
|
||||
assert.Equal(t, testData.expectedResultSecret.Type, secret.Type)
|
||||
assert.Equal(t, testData.expectedResultSecret.Annotations[VersionAnnotation], secret.Annotations[VersionAnnotation])
|
||||
}
|
||||
|
||||
//check if deployment has been restarted
|
||||
deployment := &appsv1.Deployment{}
|
||||
err = cl.Get(context.TODO(), types.NamespacedName{Name: testData.existingDeployment.Name, Namespace: namespace}, deployment)
|
||||
|
||||
_, ok := deployment.Spec.Template.Annotations[RestartAnnotation]
|
||||
if ok {
|
||||
assert.True(t, testData.expectedRestart, "Expected deployment to restart but it did not")
|
||||
} else {
|
||||
assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
|
||||
}
|
||||
|
||||
oldPodTemplateAnnotations := testData.existingDeployment.Spec.Template.ObjectMeta.Annotations
|
||||
newPodTemplateAnnotations := deployment.Spec.Template.Annotations
|
||||
for name, expected := range oldPodTemplateAnnotations {
|
||||
actual, ok := newPodTemplateAnnotations[name]
|
||||
if assert.Truef(t, ok, "Annotation %s was present in original pod template but was dropped after update", name) {
|
||||
assert.Equalf(t, expected, actual, "Annotation value for %s original pod template has changed", name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUpdatedSecret(t *testing.T) {
|
||||
|
||||
secretName := "test-secret"
|
||||
updatedSecrets := map[string]*corev1.Secret{
|
||||
"some_secret": &corev1.Secret{},
|
||||
}
|
||||
assert.False(t, isUpdatedSecret(secretName, updatedSecrets))
|
||||
|
||||
updatedSecrets[secretName] = &corev1.Secret{}
|
||||
assert.True(t, isUpdatedSecret(secretName, updatedSecrets))
|
||||
}
|
||||
|
||||
func generateFields(username, password string) []*onepassword.ItemField {
|
||||
fields := []*onepassword.ItemField{
|
||||
{
|
||||
Label: "username",
|
||||
Value: username,
|
||||
},
|
||||
{
|
||||
Label: "password",
|
||||
Value: password,
|
||||
},
|
||||
}
|
||||
return fields
|
||||
}
|
20
pkg/onepassword/uuid.go
Normal file
20
pkg/onepassword/uuid.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package onepassword
|
||||
|
||||
// UUIDLength defines the required length of UUIDs
|
||||
const UUIDLength = 26
|
||||
|
||||
// IsValidClientUUID returns true if the given client uuid is valid.
|
||||
func IsValidClientUUID(uuid string) bool {
|
||||
if len(uuid) != UUIDLength {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range uuid {
|
||||
valid := (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
29
pkg/onepassword/volumes.go
Normal file
29
pkg/onepassword/volumes.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package onepassword
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret) bool {
|
||||
for i := 0; i < len(volumes); i++ {
|
||||
if secret := volumes[i].Secret; secret != nil {
|
||||
secretName := secret.SecretName
|
||||
_, ok := secrets[secretName]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AppendUpdatedVolumeSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret {
|
||||
for i := 0; i < len(volumes); i++ {
|
||||
if secret := volumes[i].Secret; secret != nil {
|
||||
secretName := secret.SecretName
|
||||
secret, ok := secrets[secretName]
|
||||
if ok {
|
||||
updatedDeploymentSecrets[secret.Name] = secret
|
||||
}
|
||||
}
|
||||
}
|
||||
return updatedDeploymentSecrets
|
||||
}
|
43
pkg/onepassword/volumes_test.go
Normal file
43
pkg/onepassword/volumes_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package onepassword
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestAreVolmesUsingSecrets(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
"onepassword-database-secret",
|
||||
"onepassword-api-key",
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
volumes := generateVolumes(volumeSecretNames)
|
||||
|
||||
if !AreVolumesUsingSecrets(volumes, secretNamesToSearch) {
|
||||
t.Errorf("Expected that volumes were using secrets but they were not detected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAreVolumesNotUsingSecrets(t *testing.T) {
|
||||
secretNamesToSearch := map[string]*corev1.Secret{
|
||||
"onepassword-database-secret": &corev1.Secret{},
|
||||
"onepassword-api-key": &corev1.Secret{},
|
||||
}
|
||||
|
||||
volumeSecretNames := []string{
|
||||
"some_other_key",
|
||||
}
|
||||
|
||||
volumes := generateVolumes(volumeSecretNames)
|
||||
|
||||
if AreVolumesUsingSecrets(volumes, secretNamesToSearch) {
|
||||
t.Errorf("Expected that volumes were not using secrets but they were detected.")
|
||||
}
|
||||
}
|
33
pkg/utils/string.go
Normal file
33
pkg/utils/string.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ContainsString(slice []string, s string) bool {
|
||||
for _, item := range slice {
|
||||
if item == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RemoveString(slice []string, s string) (result []string) {
|
||||
for _, item := range slice {
|
||||
if item == s {
|
||||
continue
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func StringToBool(str string) (bool, error) {
|
||||
restartDeploymentBool, err := strconv.ParseBool(strings.ToLower(str))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return restartDeploymentBool, nil
|
||||
}
|
Reference in New Issue
Block a user