Compare commits

..

36 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
Jillian W
49d984c6f2 Merge pull request #64 from 1Password/release/v1.0.2
Preparing release
2021-08-27 15:32:40 -03:00
jillianwilson
72cad7284c Preparing release 2021-08-27 15:21:07 -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
Eduard Filip
6043e0da0b Merge pull request #58 from 1Password/dg/normalize-secret-name
Add secret name normalizer to the operator.
2021-08-17 20:28:07 +02:00
Eddy Filip
753cc5e9a3 Update note in README
The note now explains how the item title and fields are made into DNS subdomain-compliant names for creating Kubernetes secrets.
2021-08-16 15:24:04 +02:00
Eddy Filip
8cfe98073e Improve testing
Fix previous tests and add test for items with field names that are not valid DNS subdomain names.
2021-08-16 14:51:44 +02:00
mcmarkj
c0037526b0 remove commit file 2021-08-15 15:32:18 +01:00
david.gunter
96b42e7c52 Label normalizer now fixes both Secret names and data keys.
Each key in the `data` section of a secret must also be a valid DNS subdomain. The operator needs to "fix" the 1Password item fields before trying to create the secret.
2021-08-06 13:18:21 -07:00
david.gunter
579b5848da Add secret name normalizer to the operator.
The operator will now reformat 1Password item names to become valid names K8s Secret objects. Secret names must be a valid DNS subdomain name. See more: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
2021-08-05 16:39:55 -07: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
Eduard Filip
b50d864b50 Merge pull request #46 from 1Password/connect-deploy-custom-namespace
Add support custom namespace for connect deployment
2021-06-17 14:13:43 +03:00
Eduard Filip
1643385d9b Merge pull request #45 from 1Password/fix/makefile
Add missing argument for docker build
2021-06-17 14:13:11 +03:00
Eddy Filip
9441214733 Add support custom namespace for connect deployment
Now when the operator is deployed with the `MANAGE_CONNECT` env var set to true, the connect instance is deployed in the same namespace as the operator.
2021-06-09 20:45:33 +03:00
Eddy Filip
7e4e988813 Add missing argument for docker build
`make build` and `make build/local` would fail because the docker build commands were incomplete.
2021-06-09 16:02:05 +03:00
mcmarkj
fb1262f1bd PR Feedback' 2021-06-07 21:51:44 +01:00
Simon Barendse
68f084080e Merge pull request #40 from 1Password/feature/watch-namespaces-default
Watch all namespaces by default
2021-06-07 14:25:48 +02: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
Simon Barendse
859c9e3462 Watch all namespaces by default
When nothing is configured, watch all namespaces by default. This
makes it easier to get started.

It also makes configuring to watch all namespaces less akward
(currently you have to set the WATCH_NAMESPACE environment variable
to the empty string to configure the operator to watch all namespaces.
2021-05-14 14:26:30 +02:00
20 changed files with 442 additions and 63 deletions

View File

@@ -1 +1 @@
v1.0.1 1.1.0

View File

@@ -12,6 +12,22 @@
--- ---
[//]: # (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
## Fixes
* Name normalizer added to handle non-conforming item names.
---
[//]: # (START/v1.0.1) [//]: # (START/v1.0.1)
# v1.0.1 # v1.0.1

View File

@@ -20,12 +20,12 @@ test/coverage: ## Run test suite with coverage report
go test -v ./... -cover go test -v ./... -cover
build: ## Build operator Docker image build: ## Build operator Docker image
@docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) @docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) .
@echo "Successfully built and tagged image." @echo "Successfully built and tagged image."
@echo "Tag: $(DOCKER_IMG_TAG)" @echo "Tag: $(DOCKER_IMG_TAG)"
build/local: ## Build local version of the operator Docker image build/local: ## Build local version of the operator Docker image
@docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG) @docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG) .
build/binary: clean ## Build operator binary build/binary: clean ## Build operator binary
@mkdir -p dist @mkdir -p dist

View File

@@ -84,9 +84,9 @@ An sample Deployment yaml can be found at `/deploy/operator.yaml`.
To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml: To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml:
- **WATCH_NAMESPACE:** comma separated list of what Namespaces to watch for changes.
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect. - **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **POLLING_INTERVAL** (default: 600)**:** The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. - **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes.
- **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace. - **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. - **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
@@ -144,7 +144,12 @@ If a 1Password Item that is linked to a Kubernetes Secret is updated within the
--- ---
**NOTE** **NOTE**
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. Furthermore, titles that include white space characters cannot be used. 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:
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
- All whitespaces between words will be replaced by `-`
- All the letters will be lower-cased.
--- ---

View File

@@ -83,9 +83,11 @@ func main() {
printVersion() printVersion()
namespace, err := k8sutil.GetWatchNamespace() namespace := os.Getenv(k8sutil.WatchNamespaceEnvVar)
deploymentNamespace, err := k8sutil.GetOperatorNamespace()
if err != nil { if err != nil {
log.Error(err, "Failed to get watch namespace") log.Error(err, "Failed to get namespace")
os.Exit(1) os.Exit(1)
} }
@@ -139,7 +141,7 @@ func main() {
go func() { go func() {
connectStarted := false connectStarted := false
for connectStarted == false { for connectStarted == false {
err := op.SetupConnect(mgr.GetClient()) err := op.SetupConnect(mgr.GetClient(), deploymentNamespace)
// Cache Not Started is an acceptable error. Retry until cache is started. // Cache Not Started is an acceptable error. Retry until cache is started.
if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) {
log.Error(err, "") log.Error(err, "")

View File

@@ -2,7 +2,6 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: onepassword-connect name: onepassword-connect
namespace: default
spec: spec:
selector: selector:
matchLabels: matchLabels:
@@ -11,7 +10,7 @@ spec:
metadata: metadata:
labels: labels:
app: onepassword-connect app: onepassword-connect
version: "1.2.0" version: "1.0.0"
spec: spec:
volumes: volumes:
- name: shared-data - name: shared-data

View File

@@ -2,7 +2,6 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: onepassword-connect name: onepassword-connect
namespace: default
spec: spec:
type: NodePort type: NodePort
selector: selector:

View File

@@ -191,6 +191,7 @@ 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-path' 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
@@ -201,5 +202,5 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat
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

@@ -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,
@@ -271,6 +271,7 @@ var tests = []testReconcileItem{
op.ItemPathAnnotation: itemPath, 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,
}, },

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.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath) 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

@@ -31,6 +31,9 @@ const (
itemId = "nwrhuano7bcwddcviubpp4mhfq" itemId = "nwrhuano7bcwddcviubpp4mhfq"
username = "test-user" username = "test-user"
password = "QmHumKc$mUeEem7caHtbaBaJ" password = "QmHumKc$mUeEem7caHtbaBaJ"
firstHost = "http://localhost:8080"
awsKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
iceCream = "freezing blue 20%"
userKey = "username" userKey = "username"
passKey = "password" passKey = "password"
version = 123 version = 123
@@ -117,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,
@@ -128,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,
@@ -147,6 +152,11 @@ 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{
ItemPath: itemPath, ItemPath: itemPath,
@@ -158,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,
}, },
@@ -169,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,
}, },
@@ -210,6 +224,120 @@ var tests = []testReconcileItem{
passKey: password, passKey: password,
}, },
}, },
{
testName: "Secret from 1Password item with invalid K8s labels",
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: expectedSecretData,
},
opItem: map[string]string{
userKey: username,
passKey: password,
},
},
{
testName: "Secret from 1Password item with fields and sections that have invalid K8s labels",
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,
},
},
{
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) {
@@ -241,7 +369,10 @@ func TestReconcileOnePasswordItem(t *testing.T) {
mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) {
item := onepassword.Item{} item := onepassword.Item{}
item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) item.Fields = []*onepassword.ItemField{}
for k, v := range testData.opItem {
item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v})
}
item.Version = version item.Version = version
item.Vault.ID = vaultUUID item.Vault.ID = vaultUUID
item.ID = uuid item.ID = uuid
@@ -257,8 +388,8 @@ func TestReconcileOnePasswordItem(t *testing.T) {
// watched resource . // watched resource .
req := reconcile.Request{ req := reconcile.Request{
NamespacedName: types.NamespacedName{ NamespacedName: types.NamespacedName{
Name: name, Name: testData.customResource.ObjectMeta.Name,
Namespace: namespace, Namespace: testData.customResource.ObjectMeta.Namespace,
}, },
} }
_, err := r.Reconcile(req) _, err := r.Reconcile(req)

View File

@@ -4,12 +4,18 @@ import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings"
"github.com/1Password/connect-sdk-go/onepassword" "github.com/1Password/connect-sdk-go/onepassword"
"github.com/1Password/onepassword-operator/pkg/utils" "github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"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"
kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
) )
@@ -23,22 +29,27 @@ 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
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%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)
@@ -49,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)
} }
@@ -60,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: name, Name: formatSecretName(name),
Namespace: namespace, Namespace: namespace,
Annotations: annotations, Annotations: annotations,
Labels: labels,
}, },
Data: BuildKubernetesSecretData(item.Fields), Data: BuildKubernetesSecretData(item.Fields),
} }
@@ -75,8 +88,59 @@ 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 != "" {
secretData[fields[i].Label] = []byte(fields[i].Value) key := formatSecretDataName(fields[i].Label)
secretData[key] = []byte(fields[i].Value)
} }
} }
return secretData return secretData
} }
// formatSecretName rewrites a value to be a valid Secret name.
//
// The Secret meta.name and data keys must be valid DNS subdomain names
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
func formatSecretName(value string) string {
if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 {
return value
}
return createValidSecretName(value)
}
// formatSecretDataName rewrites a value to be a valid Secret data key.
//
// The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.`
// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets)
func formatSecretDataName(value string) string {
if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 {
return value
}
return createValidSecretDataName(value)
}
var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+")
func createValidSecretName(value string) string {
result := strings.ToLower(value)
result = invalidDNS1123Chars.ReplaceAllString(result, "-")
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
}
// first and last character MUST be alphanumeric
return strings.Trim(result, "-.")
}
var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+")
var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)")
func createValidSecretDataName(value string) string {
result := invalidStartEndChars.ReplaceAllString(value, "")
result = invalidDataChars.ReplaceAllString(result, "-")
if len(result) > kubeValidate.DNS1123SubdomainMaxLength {
result = result[0:kubeValidate.DNS1123SubdomainMaxLength]
}
return result
}

View File

@@ -9,6 +9,7 @@ import (
"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"
) )
@@ -30,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)
} }
@@ -42,6 +47,10 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
} }
compareFields(item.Fields, createdSecret.Data, t) compareFields(item.Fields, createdSecret.Data, t)
compareAnnotationsToItem(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) {
@@ -55,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)
} }
@@ -66,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)
} }
@@ -99,9 +110,10 @@ 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 != 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)
} }
if kubeSecret.Namespace != namespace { if kubeSecret.Namespace != namespace {
@@ -113,6 +125,45 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
compareFields(item.Fields, kubeSecret.Data, t) compareFields(item.Fields, kubeSecret.Data, t)
} }
func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
name := "inV@l1d k8s secret%name"
expectedName := "inv-l1d-k8s-secret-name"
namespace := "someNamespace"
annotations := map[string]string{
"annotationKey": "annotationValue",
}
labels := map[string]string{}
item := onepassword.Item{}
item.Fields = []*onepassword.ItemField{
{
Label: "label w%th invalid ch!rs-",
Value: "value1",
},
{
Label: strings.Repeat("x", kubeValidate.DNS1123SubdomainMaxLength+1),
Value: "name exceeds max length",
},
}
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, item)
// Assert Secret's meta.name was fixed
if kubeSecret.Name != expectedName {
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
}
if kubeSecret.Namespace != namespace {
t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace)
}
// assert labels were fixed for each data key
for key := range kubeSecret.Data {
if !validLabel(key) {
t.Errorf("Expected valid kubernetes label, got %s", key)
}
}
}
func compareAnnotationsToItem(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]) actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation])
if err != nil { if err != nil {
@@ -164,3 +215,10 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) {
} }
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) return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path)
} }
func validLabel(v string) bool {
if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 {
return false
}
return true
}

View File

@@ -2,6 +2,7 @@ package onepassword
import ( import (
"context" "context"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os" "os"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
@@ -17,13 +18,13 @@ var logConnectSetup = logf.Log.WithName("ConnectSetup")
var deploymentPath = "deploy/connect/deployment.yaml" var deploymentPath = "deploy/connect/deployment.yaml"
var servicePath = "deploy/connect/service.yaml" var servicePath = "deploy/connect/service.yaml"
func SetupConnect(kubeClient client.Client) error { func SetupConnect(kubeClient client.Client, deploymentNamespace string) error {
err := setupService(kubeClient, servicePath) err := setupService(kubeClient, servicePath, deploymentNamespace)
if err != nil { if err != nil {
return err return err
} }
err = setupDeployment(kubeClient, deploymentPath) err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace)
if err != nil { if err != nil {
return err return err
} }
@@ -31,22 +32,22 @@ func SetupConnect(kubeClient client.Client) error {
return nil return nil
} }
func setupDeployment(kubeClient client.Client, deploymentPath string) error { func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error {
existingDeployment := &appsv1.Deployment{} existingDeployment := &appsv1.Deployment{}
// check if deployment has already been created // check if deployment has already been created
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingDeployment) err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") logConnectSetup.Info("No existing Connect deployment found. Creating Deployment")
return createDeployment(kubeClient, deploymentPath) return createDeployment(kubeClient, deploymentPath, deploymentNamespace)
} }
} }
return err return err
} }
func createDeployment(kubeClient client.Client, deploymentPath string) error { func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error {
deployment, err := getDeploymentToCreate(deploymentPath) deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace)
if err != nil { if err != nil {
return err return err
} }
@@ -59,12 +60,16 @@ func createDeployment(kubeClient client.Client, deploymentPath string) error {
return nil return nil
} }
func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) { func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) {
f, err := os.Open(deploymentPath) f, err := os.Open(deploymentPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
deployment := &appsv1.Deployment{} deployment := &appsv1.Deployment{
ObjectMeta: v1.ObjectMeta{
Namespace: deploymentNamespace,
},
}
err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment)
if err != nil { if err != nil {
@@ -73,26 +78,30 @@ func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) {
return deployment, nil return deployment, nil
} }
func setupService(kubeClient client.Client, servicePath string) error { func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error {
existingService := &corev1.Service{} existingService := &corev1.Service{}
//check if service has already been created //check if service has already been created
err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingService) err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
logConnectSetup.Info("No existing Connect service found. Creating Service") logConnectSetup.Info("No existing Connect service found. Creating Service")
return createService(kubeClient, servicePath) return createService(kubeClient, servicePath, deploymentNamespace)
} }
} }
return err return err
} }
func createService(kubeClient client.Client, servicePath string) error { func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error {
f, err := os.Open(servicePath) f, err := os.Open(servicePath)
if err != nil { if err != nil {
return err return err
} }
service := &corev1.Service{} service := &corev1.Service{
ObjectMeta: v1.ObjectMeta{
Namespace: deploymentNamespace,
},
}
err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service)
if err != nil { if err != nil {

View File

@@ -25,7 +25,7 @@ func TestServiceSetup(t *testing.T) {
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
client := fake.NewFakeClientWithScheme(s, objs...) client := fake.NewFakeClientWithScheme(s, objs...)
err := setupService(client, "../../deploy/connect/service.yaml") err := setupService(client, "../../deploy/connect/service.yaml", defaultNamespacedName.Namespace)
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)
@@ -50,7 +50,7 @@ func TestDeploymentSetup(t *testing.T) {
// Create a fake client to mock API calls. // Create a fake client to mock API calls.
client := fake.NewFakeClientWithScheme(s, objs...) client := fake.NewFakeClientWithScheme(s, objs...)
err := setupDeployment(client, "../../deploy/connect/deployment.yaml") err := setupDeployment(client, "../../deploy/connect/deployment.yaml", defaultNamespacedName.Namespace)
if err != nil { if err != nil {
t.Errorf("Error Setting Up Connect: %v", err) t.Errorf("Error Setting Up Connect: %v", err)

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

@@ -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

@@ -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)