mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 23:48:05 +00:00
Compare commits
34 Commits
release/v1
...
v1.5.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
69857c3d47 | ||
![]() |
ad276cb296 | ||
![]() |
eab5a4ad92 | ||
![]() |
128b9b2eb3 | ||
![]() |
867e699030 | ||
![]() |
ffab2cfdab | ||
![]() |
00436b4aee | ||
![]() |
0ca3415a47 | ||
![]() |
4aa1f7a669 | ||
![]() |
6c20db47d6 | ||
![]() |
874d5c57f9 | ||
![]() |
123cfa2c86 | ||
![]() |
0796b9c5e2 | ||
![]() |
37a0f4b51e | ||
![]() |
004e0101ff | ||
![]() |
6326a856ae | ||
![]() |
1ddf92c5a0 | ||
![]() |
f5c6fa5860 | ||
![]() |
afa076d321 | ||
![]() |
d4b04c233c | ||
![]() |
ea68cfc2b4 | ||
![]() |
58b4ff8ecf | ||
![]() |
d93fecdc76 | ||
![]() |
486465247d | ||
![]() |
79868ae374 | ||
![]() |
6286f7e306 | ||
![]() |
0b5efc8690 | ||
![]() |
c00baeedcb | ||
![]() |
a760e524ea | ||
![]() |
19f774bb2d | ||
![]() |
32643651d9 | ||
![]() |
ba8d3fa698 | ||
![]() |
c57aa22a9c | ||
![]() |
48944b0d56 |
115
CHANGELOG.md
115
CHANGELOG.md
@@ -12,85 +12,136 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v1.3.0)
|
[//]: # (START/v1.5.0)
|
||||||
|
# v1.5.0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* `OnePasswordItem` now contains a `status` which contains the status of creating the kubernetes secret for a OnePasswordItem. {#52}
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* The operator no longer logs an error about changing the secret type if the secret type is not actually being changed.
|
||||||
|
* Annotations on a deployment are no longer removed when the operator triggers a restart. {#112}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[//]: # "START/v1.4.1"
|
||||||
|
|
||||||
|
# v1.4.1
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- OwnerReferences on secrets are now persisted after an item is updated. {#101}
|
||||||
|
- Annotations from a Deployment or OnePasswordItem are no longer applied to Secrets that are created for it. {#102}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[//]: # "START/v1.4.0"
|
||||||
|
|
||||||
|
# v1.4.0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The operator now declares the an OwnerReference for the secrets it manages. This should stop secrets from getting pruned by tools like Argo CD. {#51,#84,#96}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[//]: # "START/v1.3.0"
|
||||||
|
|
||||||
# v1.3.0
|
# v1.3.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Added support for loading secrets from files stored in 1Password. {#47}
|
|
||||||
|
- Added support for loading secrets from files stored in 1Password. {#47}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v1.2.0)
|
[//]: # "START/v1.2.0"
|
||||||
|
|
||||||
# v1.2.0
|
# v1.2.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Support secrets provisioned through FromEnv. {#74}
|
|
||||||
* Support configuration of Kubernetes Secret type. {#87}
|
- Support secrets provisioned through FromEnv. {#74}
|
||||||
* Improved logging. (#72)
|
- Support configuration of Kubernetes Secret type. {#87}
|
||||||
|
- Improved logging. (#72)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v1.1.0)
|
[//]: # "START/v1.1.0"
|
||||||
|
|
||||||
# v1.1.0
|
# v1.1.0
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
* Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
|
|
||||||
|
- Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v1.0.2)
|
[//]: # "START/v1.0.2"
|
||||||
|
|
||||||
# v1.0.2
|
# v1.0.2
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
* Name normalizer added to handle non-conforming item names.
|
|
||||||
|
- Name normalizer added to handle non-conforming item names.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v1.0.1)
|
[//]: # "START/v1.0.1"
|
||||||
|
|
||||||
# v1.0.1
|
# v1.0.1
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* This release also contains an arm64 Docker image. {#20}
|
|
||||||
* Docker images are also pushed to the :latest and :<major>.<minor> tags.
|
- This release also contains an arm64 Docker image. {#20}
|
||||||
|
- Docker images are also pushed to the :latest and :<major>.<minor> tags.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v1.0.0)
|
[//]: # "START/v1.0.0"
|
||||||
|
|
||||||
# v1.0.0
|
# v1.0.0
|
||||||
|
|
||||||
## Features:
|
## Features:
|
||||||
* Option to automatically deploy 1Password Connect via the operator
|
|
||||||
* Ignore restart annotation when looking for 1Password annotations
|
- Option to automatically deploy 1Password Connect via the operator
|
||||||
* Release Automation
|
- Ignore restart annotation when looking for 1Password annotations
|
||||||
* Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource
|
- Release Automation
|
||||||
* Adding configuration for auto rolling restart on deployments
|
- Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource
|
||||||
* Configure Auto Restarts for a OnePasswordItem Custom Resource
|
- Adding configuration for auto rolling restart on deployments
|
||||||
* Update Connect Dependencies to latest
|
- Configure Auto Restarts for a OnePasswordItem Custom Resource
|
||||||
* Add Github action for building and testing operator
|
- Update Connect Dependencies to latest
|
||||||
|
- Add Github action for building and testing operator
|
||||||
|
|
||||||
## Fixes:
|
## Fixes:
|
||||||
* Fix spec field example for OnePasswordItem in readme
|
|
||||||
* Casing of annotations are now consistent
|
- Fix spec field example for OnePasswordItem in readme
|
||||||
|
- Casing of annotations are now consistent
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v0.0.2)
|
[//]: # "START/v0.0.2"
|
||||||
|
|
||||||
# v0.0.2
|
# v0.0.2
|
||||||
|
|
||||||
## Features:
|
## Features:
|
||||||
* Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
|
|
||||||
|
- Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[//]: # (START/v0.0.1)
|
[//]: # "START/v0.0.1"
|
||||||
|
|
||||||
# v0.0.1
|
# v0.0.1
|
||||||
|
|
||||||
Initial 1Password Operator release
|
Initial 1Password Operator release
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret
|
|
||||||
* watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets
|
- watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret
|
||||||
* watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes
|
- watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets
|
||||||
* restart pods when secret has been updated
|
- watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes
|
||||||
* cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted
|
- restart pods when secret has been updated
|
||||||
|
- cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@@ -12,8 +12,6 @@ spec:
|
|||||||
scope: Namespaced
|
scope: Namespaced
|
||||||
versions:
|
versions:
|
||||||
- name: v1
|
- name: v1
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
schema:
|
schema:
|
||||||
openAPIV3Schema:
|
openAPIV3Schema:
|
||||||
description: OnePasswordItem is the Schema for the onepassworditems API
|
description: OnePasswordItem is the Schema for the onepassworditems API
|
||||||
@@ -38,8 +36,41 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
description: OnePasswordItemStatus defines the observed state of OnePasswordItem
|
description: OnePasswordItemStatus defines the observed state of OnePasswordItem
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
description: 'Important: Run "operator-sdk generate k8s" to regenerate
|
||||||
|
code after modifying this file Add custom validation using kubebuilder
|
||||||
|
tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html'
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: Last time the condition transit from one status
|
||||||
|
to another.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: Human-readable message indicating details about
|
||||||
|
last transition.
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: Status of the condition, one of True, False, Unknown.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: Type of job condition, Completed.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- conditions
|
||||||
type: object
|
type: object
|
||||||
type:
|
type:
|
||||||
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
|
description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
|
@@ -11,11 +11,31 @@ type OnePasswordItemSpec struct {
|
|||||||
ItemPath string `json:"itemPath,omitempty"`
|
ItemPath string `json:"itemPath,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnePasswordItemConditionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OnePasswordItemReady means the Kubernetes secret is ready for use.
|
||||||
|
OnePasswordItemReady OnePasswordItemConditionType = "Ready"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OnePasswordItemCondition struct {
|
||||||
|
// Type of job condition, Completed.
|
||||||
|
Type OnePasswordItemConditionType `json:"type"`
|
||||||
|
// Status of the condition, one of True, False, Unknown.
|
||||||
|
Status metav1.ConditionStatus `json:"status"`
|
||||||
|
// Last time the condition transit from one status to another.
|
||||||
|
// +optional
|
||||||
|
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
|
||||||
|
// Human-readable message indicating details about last transition.
|
||||||
|
// +optional
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// OnePasswordItemStatus defines the observed state of OnePasswordItem
|
// OnePasswordItemStatus defines the observed state of OnePasswordItem
|
||||||
type OnePasswordItemStatus struct {
|
type OnePasswordItemStatus struct {
|
||||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
|
||||||
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
|
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
|
||||||
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
|
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
|
||||||
|
Conditions []OnePasswordItemCondition `json:"conditions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
@@ -26,7 +46,9 @@ type OnePasswordItemStatus struct {
|
|||||||
type OnePasswordItem struct {
|
type OnePasswordItem struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
|
// Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
Spec OnePasswordItemSpec `json:"spec,omitempty"`
|
Spec OnePasswordItemSpec `json:"spec,omitempty"`
|
||||||
Status OnePasswordItemStatus `json:"status,omitempty"`
|
Status OnePasswordItemStatus `json:"status,omitempty"`
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build !ignore_autogenerated
|
||||||
// +build !ignore_autogenerated
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
// Code generated by operator-sdk. DO NOT EDIT.
|
// Code generated by operator-sdk. DO NOT EDIT.
|
||||||
@@ -14,7 +15,7 @@ func (in *OnePasswordItem) DeepCopyInto(out *OnePasswordItem) {
|
|||||||
out.TypeMeta = in.TypeMeta
|
out.TypeMeta = in.TypeMeta
|
||||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
out.Spec = in.Spec
|
out.Spec = in.Spec
|
||||||
out.Status = in.Status
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +37,23 @@ func (in *OnePasswordItem) DeepCopyObject() runtime.Object {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *OnePasswordItemCondition) DeepCopyInto(out *OnePasswordItemCondition) {
|
||||||
|
*out = *in
|
||||||
|
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordItemCondition.
|
||||||
|
func (in *OnePasswordItemCondition) DeepCopy() *OnePasswordItemCondition {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(OnePasswordItemCondition)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
|
func (in *OnePasswordItemList) DeepCopyInto(out *OnePasswordItemList) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@@ -88,6 +106,13 @@ func (in *OnePasswordItemSpec) DeepCopy() *OnePasswordItemSpec {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) {
|
func (in *OnePasswordItemStatus) DeepCopyInto(out *OnePasswordItemStatus) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.Conditions != nil {
|
||||||
|
in, out := &in.Conditions, &out.Conditions
|
||||||
|
*out = make([]OnePasswordItemCondition, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,9 +14,11 @@ import (
|
|||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
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"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
@@ -114,7 +116,7 @@ func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handles creation or updating secrets for deployment if needed
|
// Handles creation or updating secrets for deployment if needed
|
||||||
if err := r.HandleApplyingDeployment(deployment.Namespace, annotations, request); err != nil {
|
if err := r.HandleApplyingDeployment(deployment, deployment.Namespace, annotations, request); err != nil {
|
||||||
return reconcile.Result{}, err
|
return reconcile.Result{}, err
|
||||||
}
|
}
|
||||||
return reconcile.Result{}, nil
|
return reconcile.Result{}, nil
|
||||||
@@ -187,7 +189,7 @@ func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deploymen
|
|||||||
return r.kubeClient.Update(context.Background(), deployment)
|
return r.kubeClient.Update(context.Background(), deployment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotations map[string]string, request reconcile.Request) error {
|
func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error {
|
||||||
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]
|
||||||
@@ -204,5 +206,17 @@ 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], secretLabels, secretType, annotations)
|
// Create owner reference.
|
||||||
|
gvk, err := apiutil.GVKForObject(deployment, r.scheme)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not to retrieve group version kind: %v", err)
|
||||||
|
}
|
||||||
|
ownerRef := &metav1.OwnerReference{
|
||||||
|
APIVersion: gvk.GroupVersion().String(),
|
||||||
|
Kind: gvk.Kind,
|
||||||
|
Name: deployment.GetName(),
|
||||||
|
UID: deployment.GetUID(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef)
|
||||||
}
|
}
|
||||||
|
@@ -281,7 +281,6 @@ var tests = []testReconcileItem{
|
|||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
op.VersionAnnotation: fmt.Sprint(version),
|
op.VersionAnnotation: fmt.Sprint(version),
|
||||||
op.ItemPathAnnotation: itemPath,
|
op.ItemPathAnnotation: itemPath,
|
||||||
op.NameAnnotation: name,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: expectedSecretData,
|
Data: expectedSecretData,
|
||||||
@@ -294,7 +293,6 @@ var tests = []testReconcileItem{
|
|||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
op.VersionAnnotation: fmt.Sprint(version),
|
op.VersionAnnotation: fmt.Sprint(version),
|
||||||
op.ItemPathAnnotation: itemPath,
|
op.ItemPathAnnotation: itemPath,
|
||||||
op.NameAnnotation: name,
|
|
||||||
},
|
},
|
||||||
Labels: map[string]string(nil),
|
Labels: map[string]string(nil),
|
||||||
},
|
},
|
||||||
@@ -385,7 +383,7 @@ var tests = []testReconcileItem{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReconcileDepoyment(t *testing.T) {
|
func TestReconcileDeployment(t *testing.T) {
|
||||||
for _, testData := range tests {
|
for _, testData := range tests {
|
||||||
t.Run(testData.testName, func(t *testing.T) {
|
t.Run(testData.testName, func(t *testing.T) {
|
||||||
|
|
||||||
|
@@ -14,9 +14,11 @@ import (
|
|||||||
|
|
||||||
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"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
|
kubeClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
@@ -94,10 +96,11 @@ func (r *ReconcileOnePasswordItem) Reconcile(request reconcile.Request) (reconci
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handles creation or updating secrets for deployment if needed
|
// Handles creation or updating secrets for deployment if needed
|
||||||
if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil {
|
err := r.HandleOnePasswordItem(onepassworditem, request)
|
||||||
return reconcile.Result{}, err
|
if updateStatusErr := r.updateStatus(onepassworditem, err); updateStatusErr != nil {
|
||||||
|
return reconcile.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
|
||||||
}
|
}
|
||||||
return reconcile.Result{}, nil
|
return reconcile.Result{}, err
|
||||||
}
|
}
|
||||||
// If one password finalizer exists then we must cleanup associated secrets
|
// If one password finalizer exists then we must cleanup associated secrets
|
||||||
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) {
|
||||||
@@ -145,14 +148,56 @@ 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()
|
||||||
labels := resource.Labels
|
labels := resource.Labels
|
||||||
annotations := resource.Annotations
|
|
||||||
secretType := resource.Type
|
secretType := resource.Type
|
||||||
autoRestart := annotations[op.RestartDeploymentsAnnotation]
|
autoRestart := resource.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, labels, secretType, annotations)
|
// Create owner reference.
|
||||||
|
gvk, err := apiutil.GVKForObject(resource, r.scheme)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not to retrieve group version kind: %v", err)
|
||||||
|
}
|
||||||
|
ownerRef := &metav1.OwnerReference{
|
||||||
|
APIVersion: gvk.GroupVersion().String(),
|
||||||
|
Kind: gvk.Kind,
|
||||||
|
Name: resource.GetName(),
|
||||||
|
UID: resource.GetUID(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReconcileOnePasswordItem) updateStatus(resource *onepasswordv1.OnePasswordItem, err error) error {
|
||||||
|
existingCondition := findCondition(resource.Status.Conditions, onepasswordv1.OnePasswordItemReady)
|
||||||
|
updatedCondition := existingCondition
|
||||||
|
if err != nil {
|
||||||
|
updatedCondition.Message = err.Error()
|
||||||
|
updatedCondition.Status = metav1.ConditionFalse
|
||||||
|
} else {
|
||||||
|
updatedCondition.Message = ""
|
||||||
|
updatedCondition.Status = metav1.ConditionTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingCondition.Status != updatedCondition.Status {
|
||||||
|
updatedCondition.LastTransitionTime = metav1.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
resource.Status.Conditions = []onepasswordv1.OnePasswordItemCondition{updatedCondition}
|
||||||
|
return r.kubeClient.Status().Update(context.Background(), resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCondition(conditions []onepasswordv1.OnePasswordItemCondition, t onepasswordv1.OnePasswordItemConditionType) onepasswordv1.OnePasswordItemCondition {
|
||||||
|
for _, c := range conditions {
|
||||||
|
if c.Type == t {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return onepasswordv1.OnePasswordItemCondition{
|
||||||
|
Type: t,
|
||||||
|
Status: metav1.ConditionUnknown,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -101,7 +101,7 @@ var tests = []testReconcileItem{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: "Test Do not update if OnePassword Version has not changed",
|
testName: "Test Do not update if OnePassword Version or VaultPath has not changed",
|
||||||
customResource: &onepasswordv1.OnePasswordItem{
|
customResource: &onepasswordv1.OnePasswordItem{
|
||||||
TypeMeta: metav1.TypeMeta{
|
TypeMeta: metav1.TypeMeta{
|
||||||
Kind: onePasswordItemKind,
|
Kind: onePasswordItemKind,
|
||||||
|
@@ -27,7 +27,6 @@ import (
|
|||||||
const OnepasswordPrefix = "operator.1password.io"
|
const OnepasswordPrefix = "operator.1password.io"
|
||||||
const NameAnnotation = OnepasswordPrefix + "/item-name"
|
const NameAnnotation = OnepasswordPrefix + "/item-name"
|
||||||
const VersionAnnotation = OnepasswordPrefix + "/item-version"
|
const VersionAnnotation = OnepasswordPrefix + "/item-version"
|
||||||
const restartAnnotation = OnepasswordPrefix + "/last-restarted"
|
|
||||||
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
|
const ItemPathAnnotation = OnepasswordPrefix + "/item-path"
|
||||||
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
|
const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart"
|
||||||
|
|
||||||
@@ -35,29 +34,23 @@ var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type
|
|||||||
|
|
||||||
var log = logf.Log
|
var log = logf.Log
|
||||||
|
|
||||||
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string) error {
|
func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error {
|
||||||
|
|
||||||
itemVersion := fmt.Sprint(item.Version)
|
itemVersion := fmt.Sprint(item.Version)
|
||||||
|
secretAnnotations := map[string]string{
|
||||||
// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map
|
VersionAnnotation: itemVersion,
|
||||||
if secretAnnotations == nil {
|
ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID),
|
||||||
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)
|
return fmt.Errorf("Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
|
secretAnnotations[RestartDeploymentsAnnotation] = autoRestart
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Opaque" and "" secret types are treated the same by Kubernetes.
|
// "Opaque" and "" secret types are treated the same by Kubernetes.
|
||||||
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item)
|
secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef)
|
||||||
|
|
||||||
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)
|
||||||
@@ -68,32 +61,50 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
currentAnnotations := currentSecret.Annotations
|
// Check if the secret types are being changed on the update.
|
||||||
currentLabels := currentSecret.Labels
|
// Avoid Opaque and "" are treated as different on check.
|
||||||
|
wantSecretType := secretType
|
||||||
|
if wantSecretType == "" {
|
||||||
|
wantSecretType = string(corev1.SecretTypeOpaque)
|
||||||
|
}
|
||||||
currentSecretType := string(currentSecret.Type)
|
currentSecretType := string(currentSecret.Type)
|
||||||
if !reflect.DeepEqual(currentSecretType, secretType) {
|
if currentSecretType == "" {
|
||||||
|
currentSecretType = string(corev1.SecretTypeOpaque)
|
||||||
|
}
|
||||||
|
if currentSecretType != wantSecretType {
|
||||||
return ErrCannotUpdateSecretType
|
return ErrCannotUpdateSecretType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentAnnotations := currentSecret.Annotations
|
||||||
|
currentLabels := currentSecret.Labels
|
||||||
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) {
|
if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, 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 = secretAnnotations
|
currentSecret.ObjectMeta.Annotations = secretAnnotations
|
||||||
currentSecret.ObjectMeta.Labels = labels
|
currentSecret.ObjectMeta.Labels = labels
|
||||||
currentSecret.Data = secret.Data
|
currentSecret.Data = secret.Data
|
||||||
return kubeClient.Update(context.Background(), currentSecret)
|
if err := kubeClient.Update(context.Background(), currentSecret); err != nil {
|
||||||
|
return fmt.Errorf("Kubernetes secret update failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation]))
|
log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation]))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item) *corev1.Secret {
|
func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret {
|
||||||
|
var ownerRefs []metav1.OwnerReference
|
||||||
|
if ownerRef != nil {
|
||||||
|
ownerRefs = []metav1.OwnerReference{*ownerRef}
|
||||||
|
}
|
||||||
|
|
||||||
return &corev1.Secret{
|
return &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: formatSecretName(name),
|
Name: formatSecretName(name),
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Annotations: annotations,
|
Annotations: annotations,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
|
OwnerReferences: ownerRefs,
|
||||||
},
|
},
|
||||||
Data: BuildKubernetesSecretData(item.Fields, item.Files),
|
Data: BuildKubernetesSecretData(item.Fields, item.Files),
|
||||||
Type: corev1.SecretType(secretType),
|
Type: corev1.SecretType(secretType),
|
||||||
|
@@ -8,6 +8,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"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
kubeValidate "k8s.io/apimachinery/pkg/util/validation"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
@@ -32,12 +33,9 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
|||||||
|
|
||||||
kubeClient := fake.NewFakeClient()
|
kubeClient := fake.NewFakeClient()
|
||||||
secretLabels := map[string]string{}
|
secretLabels := map[string]string{}
|
||||||
secretAnnotations := map[string]string{
|
|
||||||
"testAnnotation": "exists",
|
|
||||||
}
|
|
||||||
secretType := ""
|
secretType := ""
|
||||||
|
|
||||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
|
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -49,9 +47,50 @@ 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" {
|
func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) {
|
||||||
t.Errorf("Expected testAnnotation to be merged with existing annotations, but wasn't.")
|
secretName := "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{}
|
||||||
|
secretType := ""
|
||||||
|
|
||||||
|
ownerRef := &metav1.OwnerReference{
|
||||||
|
Kind: "Deployment",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Name: "test-deployment",
|
||||||
|
UID: types.UID("test-uid"),
|
||||||
|
}
|
||||||
|
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, ownerRef)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
createdSecret := &corev1.Secret{}
|
||||||
|
err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret)
|
||||||
|
|
||||||
|
// Check owner references.
|
||||||
|
gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences
|
||||||
|
if len(gotOwnerRefs) != 1 {
|
||||||
|
t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs))
|
||||||
|
}
|
||||||
|
|
||||||
|
expOwnerRef := metav1.OwnerReference{
|
||||||
|
Kind: "Deployment",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Name: "test-deployment",
|
||||||
|
UID: types.UID("test-uid"),
|
||||||
|
}
|
||||||
|
gotOwnerRef := gotOwnerRefs[0]
|
||||||
|
if gotOwnerRef != expOwnerRef {
|
||||||
|
t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +106,9 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
|||||||
|
|
||||||
kubeClient := fake.NewFakeClient()
|
kubeClient := fake.NewFakeClient()
|
||||||
secretLabels := map[string]string{}
|
secretLabels := map[string]string{}
|
||||||
secretAnnotations := map[string]string{}
|
|
||||||
secretType := ""
|
secretType := ""
|
||||||
|
|
||||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
|
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
@@ -82,7 +120,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, secretLabels, secretType, secretAnnotations)
|
err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -118,7 +156,7 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) {
|
|||||||
labels := map[string]string{}
|
labels := map[string]string{}
|
||||||
secretType := ""
|
secretType := ""
|
||||||
|
|
||||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item)
|
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
|
||||||
if kubeSecret.Name != strings.ToLower(name) {
|
if kubeSecret.Name != strings.ToLower(name) {
|
||||||
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
|
t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name)
|
||||||
}
|
}
|
||||||
@@ -153,7 +191,7 @@ func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item)
|
kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil)
|
||||||
|
|
||||||
// Assert Secret's meta.name was fixed
|
// Assert Secret's meta.name was fixed
|
||||||
if kubeSecret.Name != expectedName {
|
if kubeSecret.Name != expectedName {
|
||||||
@@ -183,12 +221,9 @@ func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) {
|
|||||||
|
|
||||||
kubeClient := fake.NewFakeClient()
|
kubeClient := fake.NewFakeClient()
|
||||||
secretLabels := map[string]string{}
|
secretLabels := map[string]string{}
|
||||||
secretAnnotations := map[string]string{
|
|
||||||
"testAnnotation": "exists",
|
|
||||||
}
|
|
||||||
secretType := "kubernetes.io/tls"
|
secretType := "kubernetes.io/tls"
|
||||||
|
|
||||||
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations)
|
err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
v1 "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/utils"
|
"github.com/1Password/onepassword-operator/pkg/utils"
|
||||||
|
|
||||||
@@ -89,9 +91,10 @@ func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecret
|
|||||||
|
|
||||||
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) {
|
func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) {
|
||||||
log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
|
log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace))
|
||||||
deployment.Spec.Template.Annotations = map[string]string{
|
if deployment.Spec.Template.Annotations == nil {
|
||||||
RestartAnnotation: time.Now().String(),
|
deployment.Spec.Template.Annotations = map[string]string{}
|
||||||
}
|
}
|
||||||
|
deployment.Spec.Template.Annotations[RestartAnnotation] = time.Now().String()
|
||||||
err := h.client.Update(context.Background(), deployment)
|
err := h.client.Update(context.Background(), deployment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "Problem restarting deployment")
|
log.Error(err, "Problem restarting deployment")
|
||||||
@@ -116,24 +119,37 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation])
|
OnePasswordItemPath := h.getPathFromOnePasswordItem(secret)
|
||||||
|
|
||||||
|
item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name)
|
log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
itemVersion := fmt.Sprint(item.Version)
|
itemVersion := fmt.Sprint(item.Version)
|
||||||
if currentVersion != itemVersion {
|
itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID)
|
||||||
|
|
||||||
|
if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString {
|
||||||
if isItemLockedForForcedRestarts(item) {
|
if isItemLockedForForcedRestarts(item) {
|
||||||
log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName()))
|
log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName()))
|
||||||
secret.Annotations[VersionAnnotation] = itemVersion
|
secret.Annotations[VersionAnnotation] = itemVersion
|
||||||
h.client.Update(context.Background(), &secret)
|
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||||
|
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||||
|
log.Error(err, "failed to update secret %s annotations to version %d: %s", secret.Name, itemVersion, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
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, secret.Labels, string(secret.Type), *item)
|
secret.Annotations[ItemPathAnnotation] = itemPathString
|
||||||
h.client.Update(context.Background(), updatedSecret)
|
secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files)
|
||||||
|
log.Info(fmt.Sprintf("New secret path: %v and version: %v", secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation]))
|
||||||
|
if err := h.client.Update(context.Background(), &secret); err != nil {
|
||||||
|
log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -177,6 +193,22 @@ func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string
|
|||||||
return namespacesMap, nil
|
return namespacesMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string {
|
||||||
|
onePasswordItem := &v1.OnePasswordItem{}
|
||||||
|
|
||||||
|
// Search for our original OnePasswordItem if it exists
|
||||||
|
err := h.client.Get(context.TODO(), client.ObjectKey{
|
||||||
|
Namespace: secret.Namespace,
|
||||||
|
Name: secret.Name}, onePasswordItem)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return onePasswordItem.Spec.ItemPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't find the OnePassword Item we'll just return the annotation from the secret item.
|
||||||
|
return secret.Annotations[ItemPathAnnotation]
|
||||||
|
}
|
||||||
|
|
||||||
func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
|
func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool {
|
||||||
restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
|
restartDeployment := secret.Annotations[RestartDeploymentsAnnotation]
|
||||||
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
|
//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace
|
||||||
|
@@ -122,6 +122,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
@@ -235,6 +238,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Volumes: []corev1.Volume{
|
Volumes: []corev1.Volume{
|
||||||
{
|
{
|
||||||
@@ -342,6 +348,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
@@ -411,6 +420,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
@@ -482,6 +494,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
@@ -553,6 +568,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
@@ -630,6 +648,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
@@ -703,6 +724,9 @@ var tests = []testUpdateSecretTask{
|
|||||||
},
|
},
|
||||||
Spec: appsv1.DeploymentSpec{
|
Spec: appsv1.DeploymentSpec{
|
||||||
Template: corev1.PodTemplateSpec{
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{"external-annotation": "some-value"},
|
||||||
|
},
|
||||||
Spec: corev1.PodSpec{
|
Spec: corev1.PodSpec{
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
@@ -829,6 +853,16 @@ func TestUpdateSecretHandler(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
|
assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldPodTemplateAnnotations := testData.existingDeployment.Spec.Template.ObjectMeta.Annotations
|
||||||
|
newPodTemplateAnnotations := deployment.Spec.Template.Annotations
|
||||||
|
for name, expected := range oldPodTemplateAnnotations {
|
||||||
|
actual, ok := newPodTemplateAnnotations[name]
|
||||||
|
if assert.Truef(t, ok, "Annotation %s was present in original pod template but was dropped after update", name) {
|
||||||
|
assert.Equalf(t, expected, actual, "Annotation value for %s original pod template has changed", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user