Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Latest

## Features
* A user-friendly description of a new feature. {issue-number}
* Operator supports the `LABEL_SELECTOR` environment variable to restrict which `OnePasswordItem` resources it reconciles by label, so multiple operators can safely watch the same namespaces. {#276}

## Fixes
* A user-friendly description of a fix. {issue-number}
Expand Down
40 changes: 37 additions & 3 deletions USAGEGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
- [Deploy manually](#2-deploy-manually)
4. [Logging level](#logging-level)
5. [Usage examples](#usage-examples)
6. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets)
7. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
8. [Development](#development)
6. [Filtering which OnePasswordItems an operator reconciles](#filtering-which-onepassworditems-an-operator-reconciles)
7. [How 1Password Items Map to Kubernetes Secrets](#how-1password-items-map-to-kubernetes-secrets)
8. [Configuring Automatic Rolling Restarts of Deployments](#configuring-automatic-rolling-restarts-of-deployments)
9. [Development](#development)


---
Expand Down Expand Up @@ -51,6 +52,7 @@ To further configure the 1Password Kubernetes Operator the following Environment
- **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.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password. 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.
- **LABEL_SELECTOR** *(default: empty — reconcile all items)*: A [Kubernetes label selector](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) (for example `env=prod` or `app in (a,b)`). When set, the operator only reconciles `OnePasswordItem` resources whose labels match the selector. Use distinct selectors when running multiple operators that watch the same namespaces so each manages only its own items.

To deploy the operator, simply run the following command:

Expand Down Expand Up @@ -78,6 +80,7 @@ To further configure the 1Password Kubernetes Operator the following Environment
- **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 current 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.
- **LABEL_SELECTOR** *(default: empty — reconcile all items)*: A [Kubernetes label selector](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) (for example `env=prod` or `app in (a,b)`). When set, the operator only reconciles `OnePasswordItem` resources whose labels match the selector. Use distinct selectors when running multiple operators that watch the same namespaces so each manages only its own items.

---

Expand All @@ -104,6 +107,37 @@ Find usage [examples](https://developer.1password.com/docs/k8s/operator/?deploym

---

## Filtering which OnePasswordItems an operator reconciles

By default the operator reconciles every `OnePasswordItem` in the namespaces it watches. Set the `LABEL_SELECTOR` environment variable to a [Kubernetes label selector](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) to restrict it to items whose labels match.

This is useful when multiple operators watch the same namespaces but each 1Password Connect token / service account only has access to its own vaults: give each operator a distinct selector so every `OnePasswordItem` is reconciled by exactly one operator, avoiding cross-operator "token does not have access to vault" errors.

Configure the operator (Deployment `env`):

```yaml
env:
- name: LABEL_SELECTOR
value: "operator-owner=team-a"
```

Label the items that operator should manage:

```yaml
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: example-secret
labels:
operator-owner: team-a
spec:
itemPath: "vaults/<vault-id>/items/<item-id>"
```

With the selector above, this operator reconciles `example-secret` and ignores any `OnePasswordItem` that does not carry the `operator-owner=team-a` label. Set-based selectors are also supported, for example `operator-owner in (team-a,team-b)`. Leaving `LABEL_SELECTOR` empty (the default) reconciles all items.

---

## How 1Password Items Map to Kubernetes Secrets

The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.
Expand Down
26 changes: 26 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
Expand Down Expand Up @@ -73,6 +74,7 @@ const (
envPollingIntervalVariable = "POLLING_INTERVAL"
manageConnect = "MANAGE_CONNECT"
restartWorkloadsEnvVariable = "AUTO_RESTART"
labelSelectorEnvVariable = "LABEL_SELECTOR"
defaultPollingInterval = 600

annotationRegExpString = "^operator\\.1password\\.io\\/[a-zA-Z\\.]+"
Expand Down Expand Up @@ -160,6 +162,12 @@ func main() {
"the manager will watch and manage resources in all namespaces")
}

labelSelector, err := getLabelSelector()
if err != nil {
setupLog.Error(err, "unable to parse "+labelSelectorEnvVariable)
os.Exit(1)
}

deploymentNamespace, err := utils.GetOperatorNamespace()
if err != nil {
setupLog.Error(err, "Failed to get namespace")
Expand Down Expand Up @@ -305,6 +313,7 @@ func main() {
Config: controller.ReconcilerConfig{
EnableAnnotations: enableAnnotations,
AllowEmptyValues: allowEmptyValues,
LabelSelector: labelSelector,
},
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OnePasswordItem")
Expand Down Expand Up @@ -414,6 +423,23 @@ func getWatchNamespace() (string, error) {
return ns, nil
}

// getLabelSelector returns the label selector the operator should use to filter
// which OnePasswordItem resources it reconciles, parsed from the LABEL_SELECTOR
// environment variable. An unset or empty value returns a nil selector, meaning
// all OnePasswordItem resources are reconciled (the default).
func getLabelSelector() (*metav1.LabelSelector, error) {
value, found := os.LookupEnv(labelSelectorEnvVariable)
if !found || strings.TrimSpace(value) == "" {
return nil, nil
}

selector, err := metav1.ParseToLabelSelector(value)
if err != nil {
return nil, fmt.Errorf("invalid %s %q: %w", labelSelectorEnvVariable, value, err)
}
return selector, nil
}

func shouldManageConnect() bool {
shouldManageConnect, found := os.LookupEnv(manageConnect)
if found {
Expand Down
5 changes: 5 additions & 0 deletions config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ spec:
value: "10"
- name: AUTO_RESTART
value: "false"
# LABEL_SELECTOR (optional): when set, the operator only reconciles
# OnePasswordItem resources whose labels match this selector, for
# example "operator-owner=team-a". Empty means reconcile all items.
- name: LABEL_SELECTOR
value: ""
- name: OP_CONNECT_HOST
value: "http://onepassword-connect:8080"
- name: OP_CONNECT_TOKEN
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package controller

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

type ReconcilerConfig struct {
EnableAnnotations bool
AllowEmptyValues bool
// LabelSelector, when non-nil, restricts reconciliation to OnePasswordItem
// resources whose labels match the selector. A nil selector reconciles all
// items (the default).
LabelSelector *metav1.LabelSelector
}
13 changes: 12 additions & 1 deletion internal/controller/onepassworditem_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

var logOnePasswordItem = logf.Log.WithName("controller_onepassworditem")
Expand Down Expand Up @@ -135,8 +137,17 @@ func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Requ

// SetupWithManager sets up the controller with the Manager.
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
var forOpts []builder.ForOption
if r.Config.LabelSelector != nil {
pred, err := predicate.LabelSelectorPredicate(*r.Config.LabelSelector)
if err != nil {
return fmt.Errorf("building label selector predicate: %w", err)
}
forOpts = append(forOpts, builder.WithPredicates(pred))
}

return ctrl.NewControllerManagedBy(mgr).
For(&onepasswordv1.OnePasswordItem{}).
For(&onepasswordv1.OnePasswordItem{}, forOpts...).
Named("onepassworditem").
Complete(r)
}
Expand Down
58 changes: 58 additions & 0 deletions internal/controller/onepassworditem_predicate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package controller

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"

onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
)

var _ = Describe("OnePasswordItem label selector predicate", func() {
selector := metav1.LabelSelector{
MatchLabels: map[string]string{"operator.1password.io/owner": "vouchercodes"},
}

newItem := func(labels map[string]string) *onepasswordv1.OnePasswordItem {
return &onepasswordv1.OnePasswordItem{
ObjectMeta: metav1.ObjectMeta{
Name: "item",
Namespace: "default",
Labels: labels,
},
}
}

It("reconciles items whose labels match the selector", func() {
pred, err := predicate.LabelSelectorPredicate(selector)
Expect(err).ToNot(HaveOccurred())

item := newItem(map[string]string{"operator.1password.io/owner": "vouchercodes"})
Expect(pred.Create(event.CreateEvent{Object: item})).To(BeTrue())
Expect(pred.Update(event.UpdateEvent{ObjectOld: item, ObjectNew: item})).To(BeTrue())
Expect(pred.Delete(event.DeleteEvent{Object: item})).To(BeTrue())
Expect(pred.Generic(event.GenericEvent{Object: item})).To(BeTrue())
})

It("ignores items whose labels do not match the selector", func() {
pred, err := predicate.LabelSelectorPredicate(selector)
Expect(err).ToNot(HaveOccurred())

other := newItem(map[string]string{"operator.1password.io/owner": "vc-services"})
Expect(pred.Create(event.CreateEvent{Object: other})).To(BeFalse())
Expect(pred.Update(event.UpdateEvent{ObjectOld: other, ObjectNew: other})).To(BeFalse())
Expect(pred.Delete(event.DeleteEvent{Object: other})).To(BeFalse())
Expect(pred.Generic(event.GenericEvent{Object: other})).To(BeFalse())

unlabeled := newItem(nil)
Expect(pred.Create(event.CreateEvent{Object: unlabeled})).To(BeFalse())
})

It("returns an error for an invalid selector", func() {
_, err := metav1.ParseToLabelSelector("=bad=selector=")
Expect(err).To(HaveOccurred())
})
})
5 changes: 5 additions & 0 deletions test/e2e/manifests/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ spec:
value: "10"
- name: AUTO_RESTART
value: "false"
# LABEL_SELECTOR (optional): when set, the operator only reconciles
# OnePasswordItem resources whose labels match this selector, for
# example "operator-owner=team-a". Empty means reconcile all items.
- name: LABEL_SELECTOR
value: ""
- name: OP_SERVICE_ACCOUNT_TOKEN
valueFrom:
secretKeyRef:
Expand Down