Compare commits

...

42 Commits

Author SHA1 Message Date
Marton Soos
befcaae457 Fix typo in changelog 2022-02-21 11:49:14 +01:00
Marton Soos
b24aa48bd6 Add release notes for v1.2.0 2022-02-21 11:03:14 +01:00
Marton Soos
b1e251dee6 Merge pull request #74 from Nuglif/main
Verify secrets and FromEnv in addition to Env
2022-02-18 20:13:08 +01:00
Marton Soos
a34c6e8b38 Merge pull request #87 from 1Password/feature/kubernetes-secret-types
Feature: Support configuring Kubernetes secret type
2022-02-18 14:16:22 +01:00
Marton Soos
b16960057a Update tests and add new test 2022-02-18 10:47:14 +01:00
Marton Soos
285496dc7e Error when secret type is changed 2022-02-18 10:27:48 +01:00
Marton Soos
f38cf7e1c2 Fix tests and add new test 2022-02-17 21:23:22 +01:00
Marton Soos
bb7a0c8ca9 Simplify secret type cast and default to Opaque 2022-02-17 19:36:49 +01:00
Marton Soos
302653832e Account for the fact that the '' type and Opaque are equivalent on secret comparison 2022-02-17 19:18:33 +01:00
Marton Soos
a1bcfdfdcb Merge branch 'main' into feature/kubernetes-secret-types 2022-02-17 17:54:17 +01:00
Floris van der Grinten
c0f1632638 Merge pull request #72 from samifruit514/main
More logging if 1password item cant be read and continue processing other items
2021-11-18 13:39:34 +01:00
Floris van der Grinten
c46065fa7a Merge branch 'main' into samifruit514/main 2021-11-18 13:29:55 +01:00
Andres Montalban
5d229c42d5 feat: Allow configuration of the Kubernetes Secret type to be created 2021-11-18 08:32:55 -03:00
Joris Coenen
c7235b4f09 Merge pull request #49 from FabioAntunes/patch-1
Update README.md
2021-10-04 12:33:02 +02:00
Joris Coenen
5183fc129a Merge branch 'main' into patch-1 2021-10-04 12:29:48 +02:00
David Gunter
7d619165b2 Merge pull request #76 from Klaudioz/patch-1
Removing $ from bash commands
2021-09-30 09:26:03 -07:00
Claudio Canales
0363ae1e4e Removing $ from bash commands
Using the copy button is bringing the commands with a $, which is giving the error `-bash: $: command not found` after pasting them to the console.
2021-09-29 16:16:45 -03: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
Samuel Archambault
5fab662424 More logging if 1password item cant be read and continue processing others 2021-09-24 11:03:47 -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
Jillian W
49d984c6f2 Merge pull request #64 from 1Password/release/v1.0.2
Preparing release
2021-08-27 15:32:40 -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
Fábio Antunes
313cd1169b Update README.md
Minor update to the README. Got me debugging for a few hours
2021-07-02 10:23:28 +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
17 changed files with 421 additions and 54 deletions

View File

@@ -1 +1 @@
1.0.2
1.2.0

View File

@@ -12,6 +12,23 @@
---
[//]: # (START/v1.2.0)
# v1.2.0
## Features
* Support secrets provisioned through FromEnv. {#74}
* Support configuration of Kubernetes Secret type. {#87}
* Improved logging. (#72)
---
[//]: # (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)
# v1.0.2

View File

@@ -30,14 +30,13 @@ If 1Password Connect is already running, you can skip this step. This guide will
Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session:
```bash
$ cat 1password-credentials.json | base64 | \
cat 1password-credentials.json | base64 | \
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
```
Create a Kubernetes secret from the op-session file:
```bash
$ kubectl create secret generic op-credentials --from-file=1password-credentials.json
kubectl create secret generic op-credentials --from-file=1password-credentials.json=op-session
```
Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`:
@@ -53,12 +52,12 @@ Adding this environment variable will have the operator automatically deploy a d
"Create a Connect token for the operator and save it as a Kubernetes Secret:
```bash
$ kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
```
If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command:
```bash
$ kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
```
[More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens)
@@ -68,13 +67,13 @@ $ kubectl create secret generic onepassword-token --from-literal=token=$(op crea
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
```bash
$ kubectl apply -f deploy/permissions.yaml
kubectl apply -f deploy/permissions.yaml
```
**Create Custom One Password Secret Resource**
```bash
$ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
```
**Deploying the Operator**
@@ -112,13 +111,13 @@ spec:
Deploy the OnePasswordItem to Kubernetes:
```bash
$ kubectl apply -f <your_item>.yaml
kubectl apply -f <your_item>.yaml
```
To test that the Kubernetes Secret check that the following command returns a secret:
```bash
$ kubectl get secret <secret_name>
kubectl get secret <secret_name>
```
Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.

View File

@@ -178,7 +178,10 @@ func main() {
ticker.Stop()
return
case <-ticker.C:
updatedSecretsPoller.UpdateKubernetesSecretsTask()
err := updatedSecretsPoller.UpdateKubernetesSecretsTask()
if err != nil {
log.Error(err, "error running update kubernetes secret task")
}
}
}
}()

View File

@@ -39,4 +39,7 @@ spec:
status:
description: OnePasswordItemStatus defines the observed state of OnePasswordItem
type: object
type:
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
type: object

View File

@@ -26,6 +26,7 @@ type OnePasswordItemStatus struct {
type OnePasswordItem struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Type string `json:"type,omitempty"`
Spec OnePasswordItemSpec `json:"spec,omitempty"`
Status OnePasswordItemStatus `json:"status,omitempty"`

View File

@@ -191,6 +191,9 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat
reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
secretName := annotations[op.NameAnnotation]
secretLabels := map[string]string(nil)
secretType := ""
if len(secretName) == 0 {
reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.")
return nil
@@ -201,5 +204,5 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat
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, secretType, annotations)
}

View File

@@ -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{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
@@ -271,6 +271,7 @@ var tests = []testReconcileItem{
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
Labels: map[string]string{},
},
},
existingSecret: &corev1.Secret{
@@ -278,7 +279,9 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
op.NameAnnotation: name,
},
},
Data: expectedSecretData,
@@ -289,8 +292,11 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
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,
},
@@ -323,6 +329,7 @@ var tests = []testReconcileItem{
op.VersionAnnotation: "456",
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
expectedError: nil,
@@ -334,6 +341,7 @@ var tests = []testReconcileItem{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
opItem: map[string]string{
@@ -367,6 +375,7 @@ var tests = []testReconcileItem{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType(""),
Data: expectedSecretData,
},
opItem: map[string]string{

View File

@@ -3,6 +3,7 @@ package onepassworditem
import (
"context"
"fmt"
onepasswordv1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/onepassword"
@@ -143,12 +144,15 @@ func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem
func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error {
secretName := resource.GetName()
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
labels := resource.Labels
annotations := resource.Annotations
secretType := resource.Type
autoRestart := annotations[op.RestartDeploymentsAnnotation]
item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath)
if err != nil {
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, secretType, annotations)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"testing"
"github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/mocks"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
@@ -119,7 +120,8 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
@@ -130,7 +132,8 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
},
Data: expectedSecretData,
@@ -150,6 +153,11 @@ var tests = []testReconcileItem{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
@@ -160,8 +168,10 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: "456",
op.VersionAnnotation: "456",
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Data: expectedSecretData,
},
@@ -171,8 +181,10 @@ var tests = []testReconcileItem{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Data: expectedSecretData,
},
@@ -181,6 +193,59 @@ var tests = []testReconcileItem{
passKey: password,
},
},
{
testName: "Test Updating Type of Existing Kubernetes Secret using OnePasswordItem",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
Type: string(corev1.SecretTypeBasicAuth),
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Type: corev1.SecretTypeBasicAuth,
Data: expectedSecretData,
},
expectedError: nil,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
op.ItemPathAnnotation: itemPath,
},
Labels: map[string]string{},
},
Type: corev1.SecretTypeBasicAuth,
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Custom secret type",
customResource: &onepasswordv1.OnePasswordItem{
@@ -195,6 +260,7 @@ var tests = []testReconcileItem{
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
Type: "custom",
},
existingSecret: nil,
expectedError: nil,
@@ -206,6 +272,51 @@ var tests = []testReconcileItem{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretType("custom"),
Data: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Error if secret type is changed",
customResource: &onepasswordv1.OnePasswordItem{
TypeMeta: metav1.TypeMeta{
Kind: onePasswordItemKind,
APIVersion: onePasswordItemAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: onepasswordv1.OnePasswordItemSpec{
ItemPath: itemPath,
},
Type: "custom",
},
existingSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretTypeOpaque,
Data: expectedSecretData,
},
expectedError: kubernetessecrets.ErrCannotUpdateSecretType,
expectedResultSecret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
op.VersionAnnotation: fmt.Sprint(version),
},
},
Type: corev1.SecretTypeOpaque,
Data: expectedSecretData,
},
opItem: map[string]string{
@@ -274,7 +385,7 @@ var tests = []testReconcileItem{
"password": []byte(password),
"username": []byte(username),
"first-host": []byte(firstHost),
"aws-access-key": []byte(awsKey),
"AWS-Access-Key": []byte(awsKey),
"ice-cream-type": []byte(iceCream),
},
},
@@ -286,6 +397,47 @@ var tests = []testReconcileItem{
"😄 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) {

View File

@@ -7,6 +7,10 @@ import (
"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"
@@ -26,24 +30,33 @@ const restartAnnotation = OnepasswordPrefix + "/last-restarted"
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) error {
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string) error {
itemVersion := fmt.Sprint(item.Version)
annotations := map[string]string{
VersionAnnotation: itemVersion,
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID),
// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map
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 != "" {
_, err := utils.StringToBool(autoRestart)
if err != nil {
log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
return err
}
annotations[RestartDeploymentsAnnotation] = autoRestart
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
}
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, annotations, *item)
// "Opaque" and "" secret types are treated the same by Kubernetes.
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item)
currentSecret := &corev1.Secret{}
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret)
@@ -54,9 +67,17 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
return err
}
if currentSecret.Annotations[VersionAnnotation] != itemVersion {
currentAnnotations := currentSecret.Annotations
currentLabels := currentSecret.Labels
currentSecretType := string(currentSecret.Type)
if !reflect.DeepEqual(currentSecretType, secretType) {
return ErrCannotUpdateSecretType
}
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 = annotations
currentSecret.ObjectMeta.Annotations = secretAnnotations
currentSecret.ObjectMeta.Labels = labels
currentSecret.Data = secret.Data
return kubeClient.Update(context.Background(), currentSecret)
}
@@ -65,14 +86,16 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
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, secretType string, item onepassword.Item) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: formatSecretName(name),
Namespace: namespace,
Annotations: annotations,
Labels: labels,
},
Data: BuildKubernetesSecretData(item.Fields),
Type: corev1.SecretType(secretType),
}
}
@@ -80,16 +103,17 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byt
secretData := map[string][]byte{}
for i := 0; i < len(fields); i++ {
if fields[i].Value != "" {
key := formatSecretName(fields[i].Label)
key := formatSecretDataName(fields[i].Label)
secretData[key] = []byte(fields[i].Value)
}
}
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 {
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
return value
@@ -97,7 +121,18 @@ func formatSecretName(value string) string {
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 {
result := strings.ToLower(value)
@@ -108,5 +143,19 @@ func createValidSecretName(value string) string {
}
// 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

@@ -3,13 +3,13 @@ package kubernetessecrets
import (
"context"
"fmt"
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
"strings"
"testing"
"github.com/1Password/connect-sdk-go/onepassword"
corev1 "k8s.io/api/core/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"
)
@@ -31,7 +31,13 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient()
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
secretLabels := map[string]string{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -43,6 +49,10 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
}
compareFields(item.Fields, createdSecret.Data, 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) {
@@ -56,7 +66,12 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
item.ID = "h46bb3jddvay7nxopfhvlwg35q"
kubeClient := fake.NewFakeClient()
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation)
secretLabels := map[string]string{}
secretAnnotations := map[string]string{}
secretType := ""
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -67,7 +82,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
newItem.Version = 456
newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda"
newItem.ID = "h46bb3jddvay7nxopfhvlwg35q"
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation)
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@@ -100,8 +115,10 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
}
item := onepassword.Item{}
item.Fields = generateFields(5)
labels := map[string]string{}
secretType := ""
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item)
if kubeSecret.Name != strings.ToLower(name) {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
}
@@ -121,7 +138,9 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
annotations := map[string]string{
"annotationKey": "annotationValue",
}
labels := map[string]string{}
item := onepassword.Item{}
secretType := ""
item.Fields = []*onepassword.ItemField{
{
@@ -134,7 +153,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
},
}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item)
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item)
// Assert Secret's meta.name was fixed
if kubeSecret.Name != expectedName {
@@ -152,6 +171,39 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
}
}
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{}
secretAnnotations := map[string]string{
"testAnnotation": "exists",
}
secretType := "kubernetes.io/tls"
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
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 {
@@ -205,7 +257,7 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
}
func validLabel(v string) bool {
if err := kubeValidate.IsDNS1123Subdomain(v); len(err) > 0 {
if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 {
return false
}
return true

View File

@@ -1,6 +1,8 @@
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 {
for i := 0; i < len(containers); i++ {
@@ -13,6 +15,15 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string
}
}
}
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
}
@@ -28,6 +39,15 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st
}
}
}
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
}

View File

@@ -4,9 +4,10 @@ import (
"testing"
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{
"onepassword-database-secret": &corev1.Secret{},
"onepassword-api-key": &corev1.Secret{},
@@ -18,7 +19,26 @@ func TestAreContainersUsingSecrets(t *testing.T) {
"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) {
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) {
secretNamesToSearch := map[string]*corev1.Secret{
"onepassword-database-secret": &corev1.Secret{},
"onepassword-api-key": &corev1.Secret{},
"onepassword-database-secret": {},
"onepassword-api-key": {},
}
containerSecretNames := []string{
"some_other_key",
}
containers := generateContainers(containerSecretNames)
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.")
}
}

View File

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

View File

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

View File

@@ -118,7 +118,8 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation])
if err != nil {
return nil, fmt.Errorf("Failed to retrieve item: %v", err)
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)
@@ -131,7 +132,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
}
log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName()))
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, string(secret.Type), *item)
h.client.Update(context.Background(), updatedSecret)
if updatedSecrets[secret.Namespace] == nil {
updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret)