Compare commits

..

23 Commits

Author SHA1 Message Date
Marton Soos
717f9bc33f Skip shadowed env variables 2022-02-17 17:49:28 +01:00
Samuel Archambault
d9e003bdb7 cleanup comments 2021-09-24 14:02:46 -04:00
Samuel Archambault
b25f943b3a Verify secrets and FromEnv in addition to Env 2021-09-24 13:51:05 -04:00
David Gunter
d807e92c36 Merge pull request #71 from 1Password/release/v1.1.0
Prepare Release - v1.1.0
2021-09-23 11:21:16 -07:00
david.gunter
244771717c Prepare release/1.1.0 2021-09-23 11:18:53 -07:00
Floris van der Grinten
7aeb36e383 Merge pull request #66 from 1Password/fix/handling-key-names
Handling key names
2021-09-13 13:34:44 +02:00
Floris van der Grinten
5c2f840623 Merge pull request #43 from mcmarkj/pass-labels-and-annotations
Add Labels & Annotations from OPObject to Secret
2021-09-13 13:33:38 +02:00
Eddy Filip
670040477e Add max length for secret key names
Max length for secret key names must be DNS1123 compliant (253)
2021-09-08 16:02:08 +03:00
Eddy Filip
a45a310611 Make secret names DNS1123 Subdomain compiant
This is done while ensuring that secret keys are compliant (contain alphanumeric characters, `-`, `_` and `.`)
2021-09-08 15:36:40 +03:00
Eddy Filip
d80e8dd799 Add tests with names that contain . and _ 2021-09-08 13:58:48 +03:00
Eddy Filip
88728909ff Adjust regex to support _ and . and trim them
Now secret names can also contain `_` and `.` and they will be trimmed from start and end of string to be DNS1123 compliant
2021-09-08 13:49:32 +03:00
Marton Soos
e365ebfdfa Fix tests 2021-09-03 15:42:02 +03:00
Marton Soos
2c4b4df01a Do not make secret names lowercase on normalization 2021-09-03 15:41:46 +03:00
mcmarkj
0193a98681 Merge branch 'main' of github.com:1Password/onepassword-operator into pass-labels-and-annotations 2021-08-19 16:15:02 +01:00
mcmarkj
f241d7423d Use deepequal 2021-08-19 16:11:29 +01:00
mcmarkj
c0037526b0 remove commit file 2021-08-15 15:32:18 +01:00
mcmarkj
dff934cbc3 Fix tests 2021-08-04 06:33:56 +01:00
mcmarkj
2096f4440f add logic for checking for label or annotation updates 2021-08-03 21:32:04 +01:00
mcmarkj
b3fc707337 Merge branch 'main' of github.com:1Password/onepassword-operator into pass-labels-and-annotations 2021-07-23 15:29:24 +01:00
mcmarkj
fb1262f1bd PR Feedback' 2021-06-07 21:51:44 +01:00
mcmarkj
a428fe7462 GoFMT 2021-05-28 18:15:17 +01:00
mcmarkj
ea2d1f8a09 Typo 2021-05-28 18:11:10 +01:00
mcmarkj
bd96d50a9b Add Labels & Annotations from OPObject to Secret 2021-05-28 16:39:00 +01:00
23 changed files with 353 additions and 163 deletions

View File

@@ -1 +1 @@
1.0.2 1.1.0

View File

@@ -12,6 +12,14 @@
--- ---
[//]: # (START/v1.1.0)
# v1.1.0
## Fixes
* Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
---
[//]: # (START/v1.0.2) [//]: # (START/v1.0.2)
# v1.0.2 # v1.0.2

View File

@@ -2,7 +2,7 @@
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item. The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item.
The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Reference on a deployment. The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment.
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted. The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
@@ -106,7 +106,7 @@ kind: OnePasswordItem
metadata: metadata:
name: <item_name> #this name will also be used for naming the generated kubernetes secret name: <item_name> #this name will also be used for naming the generated kubernetes secret
spec: spec:
itemReference: "op://<vault_id_or_title>/<item_id_or_title>" itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
``` ```
Deploy the OnePasswordItem to Kubernetes: Deploy the OnePasswordItem to Kubernetes:
@@ -131,20 +131,20 @@ kind: Deployment
metadata: metadata:
name: deployment-example name: deployment-example
annotations: annotations:
operator.1password.io/item-reference: "op://<vault>/<item>" operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
operator.1password.io/item-name: "<secret_name>" operator.1password.io/item-name: "<secret_name>"
``` ```
Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Reference. Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path.
Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-reference` and `operator.1password.io/item-name` and no other deployment is using the secret. Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes. If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
--- ---
**NOTE** **NOTE**
If multiple 1Password vaults/items have the same `title` when using a title in the access reference, the desired action will be performed on the oldest vault/item. If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format: Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format:
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed - Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed

View File

@@ -178,8 +178,7 @@ func main() {
ticker.Stop() ticker.Stop()
return return
case <-ticker.C: case <-ticker.C:
err := updatedSecretsPoller.UpdateKubernetesSecretsTask() updatedSecretsPoller.UpdateKubernetesSecretsTask()
log.Error(err, "Error occured during update secret task")
} }
} }
}() }()

View File

@@ -33,7 +33,7 @@ spec:
spec: spec:
description: OnePasswordItemSpec defines the desired state of OnePasswordItem description: OnePasswordItemSpec defines the desired state of OnePasswordItem
properties: properties:
itemReference: itemPath:
type: string type: string
type: object type: object
status: status:

View File

@@ -3,4 +3,4 @@ kind: OnePasswordItem
metadata: metadata:
name: example name: example
spec: spec:
itemReference: "op://<vault_id>/<item_id>" itemPath: "vaults/<vault_id>/items/<item_id>"

View File

@@ -16,7 +16,6 @@ spec:
containers: containers:
- name: onepassword-connect-operator - name: onepassword-connect-operator
image: 1password/onepassword-operator image: 1password/onepassword-operator
imagePullPolicy: Never
command: ["/manager"] command: ["/manager"]
env: env:
- name: WATCH_NAMESPACE - name: WATCH_NAMESPACE

View File

@@ -8,7 +8,7 @@ import (
// OnePasswordItemSpec defines the desired state of OnePasswordItem // OnePasswordItemSpec defines the desired state of OnePasswordItem
type OnePasswordItemSpec struct { type OnePasswordItemSpec struct {
ItemReference string `json:"itemReference,omitempty"` ItemPath string `json:"itemPath,omitempty"`
} }
// OnePasswordItemStatus defines the observed state of OnePasswordItem // OnePasswordItemStatus defines the observed state of OnePasswordItem

View File

@@ -191,15 +191,16 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
secretName := annotations[op.NameAnnotation] secretName := annotations[op.NameAnnotation]
secretLabels := map[string]string(nil)
if len(secretName) == 0 { if len(secretName) == 0 {
reqLog.Info("No 'item-name' annotation set. 'item-reference' and 'item-name' must be set as annotations to add new secret.") reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
return nil return nil
} }
item, err := op.GetOnePasswordItemByReference(r.opConnectClient, annotations[op.ItemReferenceAnnotation]) item, err := op.GetOnePasswordItemByPath(r.opConnectClient, annotations[op.ItemPathAnnotation])
if err != nil { if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err) return fmt.Errorf("Failed to retrieve item: %v", err)
} }
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation]) return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, annotations)
} }

View File

@@ -52,7 +52,7 @@ var (
"password": []byte(password), "password": []byte(password),
"username": []byte(username), "username": []byte(username),
} }
ItemReference = fmt.Sprintf("op://%v/%v", vaultId, itemId) itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
) )
var ( var (
@@ -76,7 +76,7 @@ var tests = []testReconcileItem{
finalizer, finalizer,
}, },
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
}, },
@@ -90,7 +90,7 @@ var tests = []testReconcileItem{
Name: "another-deployment", Name: "another-deployment",
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
}, },
@@ -152,7 +152,7 @@ var tests = []testReconcileItem{
finalizer, finalizer,
}, },
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
}, },
@@ -166,7 +166,7 @@ var tests = []testReconcileItem{
Name: "another-deployment", Name: "another-deployment",
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
}, },
@@ -235,7 +235,7 @@ var tests = []testReconcileItem{
finalizer, finalizer,
}, },
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
}, },
@@ -258,7 +258,7 @@ var tests = []testReconcileItem{
}, },
}, },
{ {
testName: "Test Do not update if OnePassword Item Version has not changed", testName: "Test Do not update if Annotations have not changed",
deploymentResource: &appsv1.Deployment{ deploymentResource: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: deploymentKind, Kind: deploymentKind,
@@ -268,9 +268,10 @@ var tests = []testReconcileItem{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
Labels: map[string]string{},
}, },
}, },
existingSecret: &corev1.Secret{ existingSecret: &corev1.Secret{
@@ -279,6 +280,8 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -290,7 +293,10 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
}, },
Labels: map[string]string(nil),
}, },
Data: expectedSecretData, Data: expectedSecretData,
}, },
@@ -310,7 +316,7 @@ var tests = []testReconcileItem{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
}, },
@@ -352,7 +358,7 @@ var tests = []testReconcileItem{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.ItemReferenceAnnotation: ItemReference, op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name, op.NameAnnotation: name,
}, },
}, },

View File

@@ -3,7 +3,6 @@ package onepassworditem
import ( import (
"context" "context"
"fmt" "fmt"
onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/onepassword" "github.com/1Password/onepassword-operator/pkg/onepassword"
@@ -144,12 +143,14 @@ func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem
func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
secretName := resource.GetName() secretName := resource.GetName()
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation] labels := resource.Labels
annotations := resource.Annotations
autoRestart := annotations[op.RestartDeploymentsAnnotation]
item, err := onepassword.GetOnePasswordItemByReference(r.opConnectClient, resource.Spec.ItemReference) item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath)
if err != nil { if err != nil {
return fmt.Errorf("Failed to retrieve item: %v", err) return fmt.Errorf("Failed to retrieve item: %v", err)
} }
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart) return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, annotations)
} }

View File

@@ -55,7 +55,7 @@ var (
"password": []byte(password), "password": []byte(password),
"username": []byte(username), "username": []byte(username),
} }
itemReference = fmt.Sprintf("op://%v/%v", vaultId, itemId) itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
) )
var ( var (
@@ -79,7 +79,7 @@ var tests = []testReconcileItem{
}, },
}, },
Spec: onepasswordv1.OnePasswordItemSpec{ Spec: onepasswordv1.OnePasswordItemSpec{
ItemReference: itemReference, ItemPath: itemPath,
}, },
}, },
existingSecret: &corev1.Secret{ existingSecret: &corev1.Secret{
@@ -111,7 +111,7 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
}, },
Spec: onepasswordv1.OnePasswordItemSpec{ Spec: onepasswordv1.OnePasswordItemSpec{
ItemReference: itemReference, ItemPath: itemPath,
}, },
}, },
existingSecret: &corev1.Secret{ existingSecret: &corev1.Secret{
@@ -120,6 +120,7 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -131,6 +132,7 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -150,9 +152,14 @@ var tests = []testReconcileItem{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
}, },
Spec: onepasswordv1.OnePasswordItemSpec{ Spec: onepasswordv1.OnePasswordItemSpec{
ItemReference: itemReference, ItemPath: itemPath,
}, },
}, },
existingSecret: &corev1.Secret{ existingSecret: &corev1.Secret{
@@ -161,7 +168,9 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: "456", op.VersionAnnotation: "456",
op.ItemPathAnnotation: itemPath,
}, },
Labels: map[string]string{},
}, },
Data: expectedSecretData, Data: expectedSecretData,
}, },
@@ -172,7 +181,9 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version), op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
}, },
Labels: map[string]string{},
}, },
Data: expectedSecretData, Data: expectedSecretData,
}, },
@@ -193,7 +204,7 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
}, },
Spec: onepasswordv1.OnePasswordItemSpec{ Spec: onepasswordv1.OnePasswordItemSpec{
ItemReference: itemReference, ItemPath: itemPath,
}, },
}, },
existingSecret: nil, existingSecret: nil,
@@ -225,7 +236,7 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
}, },
Spec: onepasswordv1.OnePasswordItemSpec{ Spec: onepasswordv1.OnePasswordItemSpec{
ItemReference: itemReference, ItemPath: itemPath,
}, },
}, },
existingSecret: nil, existingSecret: nil,
@@ -257,7 +268,7 @@ var tests = []testReconcileItem{
Namespace: namespace, Namespace: namespace,
}, },
Spec: onepasswordv1.OnePasswordItemSpec{ Spec: onepasswordv1.OnePasswordItemSpec{
ItemReference: itemReference, ItemPath: itemPath,
}, },
}, },
existingSecret: nil, existingSecret: nil,
@@ -274,7 +285,7 @@ var tests = []testReconcileItem{
"password": []byte(password), "password": []byte(password),
"username": []byte(username), "username": []byte(username),
"first-host": []byte(firstHost), "first-host": []byte(firstHost),
"aws-access-key": []byte(awsKey), "AWS-Access-Key": []byte(awsKey),
"ice-cream-type": []byte(iceCream), "ice-cream-type": []byte(iceCream),
}, },
}, },
@@ -286,6 +297,47 @@ var tests = []testReconcileItem{
"😄 ice-cream type": iceCream, "😄 ice-cream type": iceCream,
}, },
}, },
{
testName: "Secret from 1Password item with `-`, `_` and `.`",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "!.my_sECReT.it3m%-_",
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
},
existingSecret: nil,
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret.it3m",
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Data: map[string][]byte{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"AWS-Access-Key": []byte(awsKey),
"-_ice_cream.type.": []byte(iceCream),
},
},
opItem: map[string]string{
userKey: username,
passKey: password,
"first host": firstHost,
"AWS Access Key": awsKey,
"😄 -_ice_cream.type.": iceCream,
},
},
} }
func TestReconcileOnePasswordItem(t *testing.T) { func TestReconcileOnePasswordItem(t *testing.T) {

View File

@@ -13,6 +13,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors" "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"
"reflect"
kubeValidate "k8s.io/apimachinery/pkg/util/validation" kubeValidate "k8s.io/apimachinery/pkg/util/validation"
kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -23,27 +24,32 @@ const OnepasswordPrefix = "operator.1password.io"
const NameAnnotation = OnepasswordPrefix + "/item-name" const NameAnnotation = OnepasswordPrefix + "/item-name"
const VersionAnnotation = OnepasswordPrefix + "/item-version" const VersionAnnotation = OnepasswordPrefix + "/item-version"
const restartAnnotation = OnepasswordPrefix + "/last-restarted" const restartAnnotation = OnepasswordPrefix + "/last-restarted"
const ItemReferenceAnnotation = OnepasswordPrefix + "/item-reference" const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
var log = logf.Log var log = logf.Log
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string) error { func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretAnnotations map[string]string) error {
itemVersion := fmt.Sprint(item.Version) itemVersion := fmt.Sprint(item.Version)
annotations := map[string]string{
VersionAnnotation: itemVersion, // If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map
ItemReferenceAnnotation: fmt.Sprintf("op://%v/%v", item.Vault.ID, item.ID), if secretAnnotations == nil {
secretAnnotations = map[string]string{}
} }
secretAnnotations[VersionAnnotation] = itemVersion
secretAnnotations[ItemPathAnnotation] = fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
if autoRestart != "" { if autoRestart != "" {
_, err := utils.StringToBool(autoRestart) _, err := utils.StringToBool(autoRestart)
if err != nil { if err != nil {
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
return err return err
} }
annotations[RestartDeploymentsAnnotation] = autoRestart secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
} }
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, annotations, *item) secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, *item)
currentSecret := &corev1.Secret{} currentSecret := &corev1.Secret{}
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
@@ -54,9 +60,10 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return err return err
} }
if currentSecret.Annotations[VersionAnnotation] != itemVersion { if ! reflect.DeepEqual(currentSecret.Annotations, secretAnnotations) || ! reflect.DeepEqual(currentSecret.Labels, labels) {
log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace))
currentSecret.ObjectMeta.Annotations = annotations currentSecret.ObjectMeta.Annotations = secretAnnotations
currentSecret.ObjectMeta.Labels = labels
currentSecret.Data = secret.Data currentSecret.Data = secret.Data
return kubeClient.Update(context.Background(), currentSecret) return kubeClient.Update(context.Background(), currentSecret)
} }
@@ -65,12 +72,13 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return nil return nil
} }
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, item onepassword.Item) *corev1.Secret { func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, item onepassword.Item) *corev1.Secret {
return &corev1.Secret{ return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: formatSecretName(name), Name: formatSecretName(name),
Namespace: namespace, Namespace: namespace,
Annotations: annotations, Annotations: annotations,
Labels: labels,
}, },
Data: BuildKubernetesSecretData(item.Fields), Data: BuildKubernetesSecretData(item.Fields),
} }
@@ -80,16 +88,17 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byt
secretData := map[string][]byte{} secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ { for i := 0; i < len(fields); i++ {
if fields[i].Value != "" { if fields[i].Value != "" {
key := formatSecretName(fields[i].Label) key := formatSecretDataName(fields[i].Label)
secretData[key] = []byte(fields[i].Value) secretData[key] = []byte(fields[i].Value)
} }
} }
return secretData return secretData
} }
// formatSecretName rewrites a value to be a valid Secret name or Secret data key. // 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) // 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 { func formatSecretName(value string) string {
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 { if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
return value return value
@@ -97,7 +106,18 @@ func formatSecretName(value string) string {
return createValidSecretName(value) return createValidSecretName(value)
} }
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-]+") // 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 { func createValidSecretName(value string) string {
result := strings.ToLower(value) result := strings.ToLower(value)
@@ -108,5 +128,19 @@ func createValidSecretName(value string) string {
} }
// first and last character MUST be alphanumeric // first and last character MUST be alphanumeric
return strings.Trim(result, "-") 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
} }

View File

@@ -6,11 +6,10 @@ import (
"strings" "strings"
"testing" "testing"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
) )
@@ -32,7 +31,11 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewFakeClient()
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation) secretLabels := map[string]string{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
@@ -43,7 +46,11 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
t.Errorf("Secret was not created: %v", err) t.Errorf("Secret was not created: %v", err)
} }
compareFields(item.Fields, createdSecret.Data, t) compareFields(item.Fields, createdSecret.Data, t)
compareAnnotationsToItem(item.Vault.ID, item.ID, createdSecret.Annotations, item, t) compareAnnotationsToItem(createdSecret.Annotations, item, t)
if createdSecret.Annotations["testAnnotation"] != "exists" {
t.Errorf("Expected testAnnotation to be merged with existing annotations, but wasn't.")
}
} }
func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
@@ -57,7 +64,9 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q" item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient() kubeClient := fake.NewFakeClient()
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation) secretLabels := map[string]string{}
secretAnnotations := map[string]string{}
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
@@ -68,7 +77,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
newItem.Version = 456 newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation) err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretAnnotations)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
@@ -79,7 +88,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
t.Errorf("Secret was not found: %v", err) t.Errorf("Secret was not found: %v", err)
} }
compareFields(newItem.Fields, updatedSecret.Data, t) compareFields(newItem.Fields, updatedSecret.Data, t)
compareAnnotationsToItem(newItem.Vault.ID, newItem.ID, updatedSecret.Annotations, newItem, t) compareAnnotationsToItem(updatedSecret.Annotations, newItem, t)
} }
func TestBuildKubernetesSecretData(t *testing.T) { func TestBuildKubernetesSecretData(t *testing.T) {
fields := generateFields(5) fields := generateFields(5)
@@ -101,8 +110,9 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
} }
item := onepassword.Item{} item := onepassword.Item{}
item.Fields = generateFields(5) item.Fields = generateFields(5)
labels := map[string]string{}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item) kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
if kubeSecret.Name != strings.ToLower(name) { if kubeSecret.Name != strings.ToLower(name) {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
} }
@@ -122,6 +132,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
annotations := map[string]string{ annotations := map[string]string{
"annotationKey": "annotationValue", "annotationKey": "annotationValue",
} }
labels := map[string]string{}
item := onepassword.Item{} item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{ item.Fields = []*onepassword.ItemField{
@@ -135,7 +146,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
}, },
} }
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item) kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
// Assert Secret's meta.name was fixed // Assert Secret's meta.name was fixed
if kubeSecret.Name != expectedName { if kubeSecret.Name != expectedName {
@@ -153,7 +164,11 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
} }
} }
func compareAnnotationsToItem(actualVaultId, actualItemId string, annotations map[string]string, item onepassword.Item, t *testing.T) { 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 { if actualVaultId != item.Vault.ID {
t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId) t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId)
} }
@@ -193,8 +208,16 @@ func generateFields(numToGenerate int) []*onepassword.ItemField {
return fields 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 { func validLabel(v string) bool {
if err := kubeValidate.IsDNS1123Subdomain(v); len(err) > 0 { if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 {
return false return false
} }
return true return true

View File

@@ -9,7 +9,7 @@ import (
const ( const (
OnepasswordPrefix = "operator.1password.io" OnepasswordPrefix = "operator.1password.io"
ItemReferenceAnnotation = OnepasswordPrefix + "/item-reference" ItemPathAnnotation = OnepasswordPrefix + "/item-path"
NameAnnotation = OnepasswordPrefix + "/item-name" NameAnnotation = OnepasswordPrefix + "/item-name"
VersionAnnotation = OnepasswordPrefix + "/item-version" VersionAnnotation = OnepasswordPrefix + "/item-version"
RestartAnnotation = OnepasswordPrefix + "/last-restarted" RestartAnnotation = OnepasswordPrefix + "/last-restarted"

View File

@@ -22,7 +22,7 @@ func TestFilterAnnotations(t *testing.T) {
if len(filteredAnnotations) != 2 { if len(filteredAnnotations) != 2 {
t.Errorf("Unexpected number of filtered annotations returned. Expected 2, got %v", len(filteredAnnotations)) t.Errorf("Unexpected number of filtered annotations returned. Expected 2, got %v", len(filteredAnnotations))
} }
_, found := filteredAnnotations[ItemReferenceAnnotation] _, found := filteredAnnotations[ItemPathAnnotation]
if !found { if !found {
t.Errorf("One Password Annotation was filtered when it should not have been") t.Errorf("One Password Annotation was filtered when it should not have been")
} }
@@ -87,7 +87,7 @@ func TestGetNoAnnotationsForDeployment(t *testing.T) {
func getValidAnnotations() map[string]string { func getValidAnnotations() map[string]string {
return map[string]string{ return map[string]string{
ItemReferenceAnnotation: "op://b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f", ItemPathAnnotation: "vaults/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f/items/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f",
NameAnnotation: "secretName", NameAnnotation: "secretName",
} }
} }

View File

@@ -1,11 +1,16 @@
package onepassword package onepassword
import corev1 "k8s.io/api/core/v1" import (
corev1 "k8s.io/api/core/v1"
)
func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool { func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool {
for i := 0; i < len(containers); i++ { for i := 0; i < len(containers); i++ {
envVariables := containers[i].Env envVariables := containers[i].Env
envVariableNames := map[string]struct{}{}
for j := 0; j < len(envVariables); j++ { for j := 0; j < len(envVariables); j++ {
envVariableNames[envVariables[j].Name] = struct{}{}
if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil { if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil {
_, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name] _, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name]
if ok { if ok {
@@ -13,6 +18,19 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
} }
} }
} }
envFromVariables := containers[i].EnvFrom
for j := 0; j < len(envFromVariables); j++ {
if envFromVariables[j].SecretRef != nil {
// Skip env variables that will be overwritten by Env
if _, ok := envVariableNames[envFromVariables[i].SecretRef.Name]; ok {
continue;
}
_, ok := secrets[envFromVariables[j].SecretRef.Name]
if ok {
return true
}
}
}
} }
return false return false
} }
@@ -20,7 +38,10 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { 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++ { for i := 0; i < len(containers); i++ {
envVariables := containers[i].Env envVariables := containers[i].Env
envVariableNames := map[string]struct{}{}
for j := 0; j < len(envVariables); j++ { for j := 0; j < len(envVariables); j++ {
envVariableNames[envVariables[j].Name] = struct{}{}
if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil { if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil {
secret, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name] secret, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name]
if ok { if ok {
@@ -28,6 +49,19 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
} }
} }
} }
envFromVariables := containers[i].EnvFrom
for j := 0; j < len(envFromVariables); j++ {
if envFromVariables[j].SecretRef != nil {
// Skip env variables that will be overwritten by Env
if _, ok := envVariableNames[envFromVariables[i].SecretRef.Name]; ok {
continue;
}
secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name]
if ok {
updatedDeploymentSecrets[secret.Name] = secret
}
}
}
} }
return updatedDeploymentSecrets return updatedDeploymentSecrets
} }

View File

@@ -4,9 +4,10 @@ import (
"testing" "testing"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
func TestAreContainersUsingSecrets(t *testing.T) { func TestAreContainersUsingSecretsFromEnv(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": &corev1.Secret{},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": &corev1.Secret{},
@@ -18,7 +19,26 @@ func TestAreContainersUsingSecrets(t *testing.T) {
"some_other_key", "some_other_key",
} }
containers := generateContainers(containerSecretNames) 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) { if !AreContainersUsingSecrets(containers, secretNamesToSearch) {
t.Errorf("Expected that containers were using secrets but they were not detected.") t.Errorf("Expected that containers were using secrets but they were not detected.")
@@ -27,17 +47,39 @@ func TestAreContainersUsingSecrets(t *testing.T) {
func TestAreContainersNotUsingSecrets(t *testing.T) { func TestAreContainersNotUsingSecrets(t *testing.T) {
secretNamesToSearch := map[string]*corev1.Secret{ secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{}, "onepassword-database-secret": {},
"onepassword-api-key": &corev1.Secret{}, "onepassword-api-key": {},
} }
containerSecretNames := []string{ containerSecretNames := []string{
"some_other_key", "some_other_key",
} }
containers := generateContainers(containerSecretNames) containers := generateContainersWithSecretRefsFromEnv(containerSecretNames)
if AreContainersUsingSecrets(containers, secretNamesToSearch) { if AreContainersUsingSecrets(containers, secretNamesToSearch) {
t.Errorf("Expected that containers were not using secrets but they were detected.") 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.")
}
}

View File

@@ -39,7 +39,7 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) {
} }
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{}
deployment.Spec.Template.Spec.Containers = generateContainers(containerSecretNames) deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames)
if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) {
t.Errorf("Expected that deployment was using secrets but they were not detected.") t.Errorf("Expected that deployment was using secrets but they were not detected.")
} }

View File

@@ -11,16 +11,11 @@ import (
var logger = logf.Log.WithName("retrieve_item") var logger = logf.Log.WithName("retrieve_item")
const ( func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) {
secretReferencePrefix = "op://" vaultValue, itemValue, err := ParseVaultAndItemFromPath(path)
)
func GetOnePasswordItemByReference(opConnectClient connect.Client, reference string) (*onepassword.Item, error) {
vaultValue, itemValue, err := ParseReference(reference)
if err != nil { if err != nil {
return nil, err return nil, err
} }
vaultId, err := getVaultId(opConnectClient, vaultValue) vaultId, err := getVaultId(opConnectClient, vaultValue)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -38,28 +33,12 @@ func GetOnePasswordItemByReference(opConnectClient connect.Client, reference str
return item, nil return item, nil
} }
func ParseReference(reference string) (string, string, error) { func ParseVaultAndItemFromPath(path string) (string, string, error) {
if !strings.HasPrefix(reference, secretReferencePrefix) {
return "", "", fmt.Errorf("secret reference should start with `op://`")
}
path := strings.TrimPrefix(reference, secretReferencePrefix)
splitPath := strings.Split(path, "/") splitPath := strings.Split(path, "/")
if len(splitPath) != 2 { if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" {
return "", "", fmt.Errorf("Invalid secret reference : %s. Secret references should match op://<vault>/<item>", reference) 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)
vault := splitPath[0]
if vault == "" {
return "", "", fmt.Errorf("Invalid secret reference : %s. Vault can't be empty.", reference)
}
item := splitPath[1]
if item == "" {
return "", "", fmt.Errorf("Invalid secret reference : %s. Item can't be empty.", reference)
}
return vault, item, nil
} }
func getVaultId(client connect.Client, vaultIdentifier string) (string, error) { func getVaultId(client connect.Client, vaultIdentifier string) (string, error) {

View File

@@ -17,8 +17,7 @@ func generateVolumes(names []string) []corev1.Volume {
} }
return volumes return volumes
} }
func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container {
func generateContainers(names []string) []corev1.Container {
containers := []corev1.Container{} containers := []corev1.Container{}
for i := 0; i < len(names); i++ { for i := 0; i < len(names); i++ {
container := corev1.Container{ container := corev1.Container{
@@ -40,3 +39,16 @@ func generateContainers(names []string) []corev1.Container {
} }
return containers 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
}

View File

@@ -110,13 +110,13 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
for i := 0; i < len(secrets.Items); i++ { for i := 0; i < len(secrets.Items); i++ {
secret := secrets.Items[i] secret := secrets.Items[i]
itemReference := secret.Annotations[ItemReferenceAnnotation] itemPath := secret.Annotations[ItemPathAnnotation]
currentVersion := secret.Annotations[VersionAnnotation] currentVersion := secret.Annotations[VersionAnnotation]
if len(itemReference) == 0 || len(currentVersion) == 0 { if len(itemPath) == 0 || len(currentVersion) == 0 {
continue continue
} }
item, err := GetOnePasswordItemByReference(h.opConnectClient, secret.Annotations[ItemReferenceAnnotation]) item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation])
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to retrieve item: %v", err) return nil, fmt.Errorf("Failed to retrieve item: %v", err)
} }
@@ -131,7 +131,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
} }
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
secret.Annotations[VersionAnnotation] = itemVersion secret.Annotations[VersionAnnotation] = itemVersion
updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item) updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, *item)
h.client.Update(context.Background(), updatedSecret) h.client.Update(context.Background(), updatedSecret)
if updatedSecrets[secret.Namespace] == nil { if updatedSecrets[secret.Namespace] == nil {
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)

View File

@@ -51,7 +51,7 @@ var (
"password": []byte(password), "password": []byte(password),
"username": []byte(username), "username": []byte(username),
} }
itemReference = fmt.Sprintf("op://%v/%v", vaultId, itemId) itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId)
) )
var defaultNamespace = &corev1.Namespace{ var defaultNamespace = &corev1.Namespace{
@@ -74,7 +74,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
NameAnnotation: "unlrelated secret", NameAnnotation: "unlrelated secret",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
}, },
@@ -84,7 +84,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -96,7 +96,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -150,7 +150,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -162,7 +162,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -186,7 +186,7 @@ var tests = []testUpdateSecretTask{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
NameAnnotation: name, NameAnnotation: name,
}, },
}, },
@@ -197,7 +197,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -209,7 +209,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -256,7 +256,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -268,7 +268,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -292,7 +292,7 @@ var tests = []testUpdateSecretTask{
Name: name, Name: name,
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
NameAnnotation: name, NameAnnotation: name,
}, },
}, },
@@ -303,7 +303,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -315,7 +315,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -370,7 +370,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -382,7 +382,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -439,7 +439,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
RestartDeploymentsAnnotation: "true", RestartDeploymentsAnnotation: "true",
}, },
}, },
@@ -452,7 +452,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
RestartDeploymentsAnnotation: "true", RestartDeploymentsAnnotation: "true",
}, },
}, },
@@ -510,7 +510,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
RestartDeploymentsAnnotation: "false", RestartDeploymentsAnnotation: "false",
}, },
}, },
@@ -523,7 +523,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
RestartDeploymentsAnnotation: "false", RestartDeploymentsAnnotation: "false",
}, },
}, },
@@ -581,7 +581,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -593,7 +593,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -658,7 +658,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -670,7 +670,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -731,7 +731,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: "old version", VersionAnnotation: "old version",
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,
@@ -743,7 +743,7 @@ var tests = []testUpdateSecretTask{
Namespace: namespace, Namespace: namespace,
Annotations: map[string]string{ Annotations: map[string]string{
VersionAnnotation: fmt.Sprint(itemVersion), VersionAnnotation: fmt.Sprint(itemVersion),
ItemReferenceAnnotation: itemReference, ItemPathAnnotation: itemPath,
}, },
}, },
Data: expectedSecretData, Data: expectedSecretData,