mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 07:28:06 +00:00
Compare commits
2 Commits
restart-on
...
autorestar
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2fa035022c | ||
![]() |
d715a6ed0e |
@@ -13,6 +13,14 @@ var logger = logf.Log.WithName("retrieve_item")
|
|||||||
|
|
||||||
const secretReferencePrefix = "op://"
|
const secretReferencePrefix = "op://"
|
||||||
|
|
||||||
|
type InvalidOPFormatError struct {
|
||||||
|
Reference string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidOPFormatError) Error() string {
|
||||||
|
return fmt.Sprintf("Invalid secret reference : %s. Secret references should start with op://", e.Reference)
|
||||||
|
}
|
||||||
|
|
||||||
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
|
func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
|
||||||
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
|
vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -37,7 +45,7 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one
|
|||||||
|
|
||||||
func ParseReference(reference string) (string, string, error) {
|
func ParseReference(reference string) (string, string, error) {
|
||||||
if !strings.HasPrefix(reference, secretReferencePrefix) {
|
if !strings.HasPrefix(reference, secretReferencePrefix) {
|
||||||
return "", "", fmt.Errorf("secret reference should start with `op://`")
|
return "", "", &InvalidOPFormatError{Reference: reference}
|
||||||
}
|
}
|
||||||
path := strings.TrimPrefix(reference, secretReferencePrefix)
|
path := strings.TrimPrefix(reference, secretReferencePrefix)
|
||||||
|
|
||||||
|
@@ -2,12 +2,13 @@ package onepassword
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/1Password/connect-sdk-go/connect"
|
"github.com/1Password/connect-sdk-go/connect"
|
||||||
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
|
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
|
||||||
"github.com/1Password/onepassword-operator/operator/pkg/utils"
|
"github.com/1Password/onepassword-operator/operator/pkg/utils"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
||||||
@@ -41,6 +42,10 @@ func CreateOnePasswordCRSecretsFromContainer(opClient connect.Client, kubeClient
|
|||||||
// if value is not of format op://<vault>/<item>/<field> then ignore
|
// if value is not of format op://<vault>/<item>/<field> then ignore
|
||||||
vault, item, err := ParseReference(env.Value)
|
vault, item, err := ParseReference(env.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
var ev *InvalidOPFormatError
|
||||||
|
if !errors.As(err, &ev) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// create a one password item custom resource to track updates for injected secrets
|
// create a one password item custom resource to track updates for injected secrets
|
||||||
@@ -64,7 +69,7 @@ func CreateOnePasswordCRSecretFromReference(opClient connect.Client, kubeClient
|
|||||||
|
|
||||||
currentOnepassworditem := &onepasswordv1.OnePasswordItem{}
|
currentOnepassworditem := &onepasswordv1.OnePasswordItem{}
|
||||||
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: onepassworditem.Name, Namespace: onepassworditem.Namespace}, currentOnepassworditem)
|
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: onepassworditem.Name, Namespace: onepassworditem.Namespace}, currentOnepassworditem)
|
||||||
if err != nil && errors.IsNotFound(err) {
|
if k8sErrors.IsNotFound(err) {
|
||||||
log.Info(fmt.Sprintf("Creating OnePasswordItem CR %v at namespace '%v'", onepassworditem.Name, onepassworditem.Namespace))
|
log.Info(fmt.Sprintf("Creating OnePasswordItem CR %v at namespace '%v'", onepassworditem.Name, onepassworditem.Namespace))
|
||||||
return kubeClient.Create(context.Background(), onepassworditem)
|
return kubeClient.Create(context.Background(), onepassworditem)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
234
operator/pkg/onepassword/onepassword_item_test.go
Normal file
234
operator/pkg/onepassword/onepassword_item_test.go
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
package onepassword
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Password/onepassword-operator/operator/pkg/mocks"
|
||||||
|
|
||||||
|
"github.com/1Password/connect-sdk-go/onepassword"
|
||||||
|
onepasswordv1 "github.com/1Password/onepassword-operator/operator/pkg/apis/onepassword/v1"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type onepassworditemInjections struct {
|
||||||
|
testName string
|
||||||
|
existingDeployment *appsv1.Deployment
|
||||||
|
existingNamespace *corev1.Namespace
|
||||||
|
expectedError error
|
||||||
|
expectedEvents []string
|
||||||
|
opItem map[string]string
|
||||||
|
expectedOPItem *onepasswordv1.OnePasswordItem
|
||||||
|
}
|
||||||
|
|
||||||
|
var onepassworditemTests = []onepassworditemInjections{
|
||||||
|
{
|
||||||
|
testName: "Try to Create OnePasswordItem with container with valid op reference",
|
||||||
|
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{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
ContainerInjectAnnotation: "test-app",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "test-app",
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
Value: fmt.Sprintf("op://%s/%s/test", vaultId, itemId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
opItem: map[string]string{
|
||||||
|
userKey: username,
|
||||||
|
passKey: password,
|
||||||
|
},
|
||||||
|
expectedOPItem: &onepasswordv1.OnePasswordItem{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "OnePasswordItem",
|
||||||
|
APIVersion: "onepassword.com/v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: injectedOnePasswordItemName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
InjectedAnnotation: "true",
|
||||||
|
VersionAnnotation: "old",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: onepasswordv1.OnePasswordItemSpec{
|
||||||
|
ItemPath: itemPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Container with no op:// reference does not create OnePasswordItem",
|
||||||
|
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{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
ContainerInjectAnnotation: "test-app",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "test-app",
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
Value: fmt.Sprintf("some value"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
opItem: map[string]string{
|
||||||
|
userKey: username,
|
||||||
|
passKey: password,
|
||||||
|
},
|
||||||
|
expectedOPItem: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Container with op:// reference missing vault and item does not create OnePasswordItem and returns error",
|
||||||
|
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{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
ContainerInjectAnnotation: "test-app",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "test-app",
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
Value: fmt.Sprintf("op://"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: fmt.Errorf("Invalid secret reference : %s. Secret references should match op://<vault>/<item>/<field>", "op://"),
|
||||||
|
opItem: map[string]string{
|
||||||
|
userKey: username,
|
||||||
|
passKey: password,
|
||||||
|
},
|
||||||
|
expectedOPItem: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOnePasswordItemSecretInjected(t *testing.T) {
|
||||||
|
for _, testData := range onepassworditemTests {
|
||||||
|
t.Run(testData.testName, func(t *testing.T) {
|
||||||
|
|
||||||
|
// Register operator types with the runtime scheme.
|
||||||
|
s := scheme.Scheme
|
||||||
|
s.AddKnownTypes(appsv1.SchemeGroupVersion, &onepasswordv1.OnePasswordItem{}, &onepasswordv1.OnePasswordItemList{}, &appsv1.Deployment{})
|
||||||
|
|
||||||
|
// Objects to track in the fake client.
|
||||||
|
objs := []runtime.Object{
|
||||||
|
testData.existingDeployment,
|
||||||
|
testData.existingNamespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
injectedContainers := testData.existingDeployment.Spec.Template.ObjectMeta.Annotations[ContainerInjectAnnotation]
|
||||||
|
parsedInjectedContainers := strings.Split(injectedContainers, ",")
|
||||||
|
err := CreateOnePasswordItemResourceFromDeployment(opConnectClient, cl, testData.existingDeployment, parsedInjectedContainers)
|
||||||
|
|
||||||
|
assert.Equal(t, testData.expectedError, err)
|
||||||
|
|
||||||
|
// Check if Secret has been created and has the correct data
|
||||||
|
opItemCR := &onepasswordv1.OnePasswordItem{}
|
||||||
|
err = cl.Get(context.TODO(), types.NamespacedName{Name: injectedOnePasswordItemName, Namespace: namespace}, opItemCR)
|
||||||
|
|
||||||
|
if testData.expectedOPItem == nil {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, errors2.IsNotFound(err))
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, testData.expectedOPItem.Spec.ItemPath, opItemCR.Spec.ItemPath)
|
||||||
|
assert.Equal(t, testData.expectedOPItem.Name, opItemCR.Name)
|
||||||
|
assert.Equal(t, testData.expectedOPItem.Annotations[InjectedAnnotation], opItemCR.Annotations[InjectedAnnotation])
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -162,14 +162,14 @@ func (h *SecretUpdateHandler) updateInjectedSecrets() (map[string]map[string]*on
|
|||||||
onepasswordItems := &onepasswordv1.OnePasswordItemList{}
|
onepasswordItems := &onepasswordv1.OnePasswordItemList{}
|
||||||
err := h.client.List(context.Background(), onepasswordItems)
|
err := h.client.List(context.Background(), onepasswordItems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "Failed to list OneOasswordItems")
|
log.Error(err, "Failed to list OnePasswordItems")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedItems := map[string]map[string]*onepasswordv1.OnePasswordItem{}
|
updatedItems := map[string]map[string]*onepasswordv1.OnePasswordItem{}
|
||||||
for _, item := range onepasswordItems.Items {
|
for _, item := range onepasswordItems.Items {
|
||||||
|
|
||||||
// if onepassworditem was generated by injecting a secret into a deployment then ignore
|
// if onepassworditem was not generated by injecting a secret into a deployment then ignore
|
||||||
_, injected := item.Annotations[InjectedAnnotation]
|
_, injected := item.Annotations[InjectedAnnotation]
|
||||||
if !injected {
|
if !injected {
|
||||||
continue
|
continue
|
||||||
|
Reference in New Issue
Block a user