Files
onepassword-operator/internal/controller/onepassworditem_controller.go
Eduard Filip cabc020cc6 Upgrade to Operator SDK 1.41.1 (#211)
* Add missing improvements from Operator SDK 1.34.1

These were not mentioned in the upgrade documentation for version 1.34.x (https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.34.0/), but I've found them by compating the release with the previous one (https://github.com/operator-framework/operator-sdk/compare/v1.33.0...v1.34.1).

* Upgrade to Operator SDK 1.36.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.36.0/
Key differences:
- Go packages `k8s.io/*` are already at a version higher than the one in the upgrade.
- `ENVTEST_K8S_VERSION` is at a version higher than the one in the upgrade
- We didn't have the golangci-lint make command before, thus we only needed to add things.

* Upgrade to Operator SDK 1.38.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.38.0/

* Upgrade to Operator SDK 1.39.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.39.0/

* Upgrade to Operator SDK 1.40.0

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.40.0/

I didn't do the "Add app.kubernetes.io/name label to your manifests" since it seems that we have it already, and it's customized.

* Address lint errors

* Update golangci-lint version used to support Go 1.24

* Improve workflows

- Make workflow targets more specific.
- Make build workflow only build (i.e. remove test part of it).
- Rearrange steps and improve naming for build workflow.

* Add back deleted test

Initially the test has been removed due to lint saying that it was duplicate code, but it falsely errored since the values are different.

* Improve code and add missing upgrade pieces

* Upgrade to Operator SDK 1.41.1

Source of upgrade steps: https://sdk.operatorframework.io/docs/upgrading-sdk-version/v1.41.0/

Upgrading to 1.41.1 from 1.40.0 doesn't have any migration steps.

Key elements:
- Upgrade to golangci-lint v2
- Made the manifests using the updated controller tools

* Address linter errors

golanci-lint v2 seems to be more robust than the previous one, which is beneficial. Thus, we address the linter errors thrown by v2 and improve our code even further.

* Add Makefile improvements

These were brought in by comparing the Makefile of a freshly created operator using the latest operator-sdk with ours.

* Add missing default kustomization for 1.40.0 upgrade

* Bring default kustomization to latest version

This is done by putting the file's content from a newly-generated operator.

* Switch metrics-bind-address default value back to 8080

This ensures that the upgrade is backwards-compatible.

* Add webhook-related scaffolding

This enables us to easily add support for webhooks by running `operator-sdk create webhook` whenever we want to add them.

* Fix typo
2025-07-14 19:32:30 +02:00

217 lines
8.7 KiB
Go

/*
MIT License
Copyright (c) 2020-2024 1Password
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package controller
import (
"context"
"fmt"
"strings"
"time"
onepasswordv1 "github.com/1Password/onepassword-operator/api/v1"
kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets"
"github.com/1Password/onepassword-operator/pkg/logs"
op "github.com/1Password/onepassword-operator/pkg/onepassword"
opclient "github.com/1Password/onepassword-operator/pkg/onepassword/client"
"github.com/1Password/onepassword-operator/pkg/utils"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
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/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)
var logOnePasswordItem = logf.Log.WithName("controller_onepassworditem")
var finalizer = "onepassword.com/finalizer.secret"
// OnePasswordItemReconciler reconciles a OnePasswordItem object
type OnePasswordItemReconciler struct {
client.Client
Scheme *runtime.Scheme
OpClient opclient.Client
}
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=onepassword.com,resources=onepassworditems/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=pods,verbs=get
// +kubebuilder:rbac:groups="",resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;namespaces,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;replicasets;statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=replicasets;deployments,verbs=get
// +kubebuilder:rbac:groups=apps,resourceNames=onepassword-connect-operator,resources=deployments/finalizers,verbs=update
// +kubebuilder:rbac:groups=onepassword.com,resources=*,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the OnePasswordItem object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reqLogger := logOnePasswordItem.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
reqLogger.V(logs.DebugLevel).Info("Reconciling OnePasswordItem")
onepassworditem := &onepasswordv1.OnePasswordItem{}
err := r.Get(ctx, req.NamespacedName, onepassworditem)
if err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// If the deployment is not being deleted
if onepassworditem.DeletionTimestamp.IsZero() {
// Adds a finalizer to the deployment if one does not exist.
// This is so we can handle cleanup of associated secrets properly
if !utils.ContainsString(onepassworditem.Finalizers, finalizer) {
onepassworditem.Finalizers = append(onepassworditem.Finalizers, finalizer)
if err = r.Update(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err
}
}
// Handles creation or updating secrets for deployment if needed
err = r.handleOnePasswordItem(ctx, onepassworditem, req)
if err != nil {
if strings.Contains(err.Error(), "rate limit") {
reqLogger.V(logs.InfoLevel).Info("1Password rate limit hit. Requeuing after 15 minutes.")
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
}
}
if updateStatusErr := r.updateStatus(ctx, onepassworditem, err); updateStatusErr != nil {
return ctrl.Result{}, fmt.Errorf("cannot update status: %s", updateStatusErr)
}
return ctrl.Result{}, err
}
// If one password finalizer exists then we must cleanup associated secrets
if utils.ContainsString(onepassworditem.Finalizers, finalizer) {
// Delete associated kubernetes secret
if err = r.cleanupKubernetesSecret(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err
}
// Remove finalizer now that cleanup is complete
if err = r.removeOnePasswordFinalizerFromOnePasswordItem(ctx, onepassworditem); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&onepasswordv1.OnePasswordItem{}).
Named("onepassworditem").
Complete(r)
}
func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
kubernetesSecret := &corev1.Secret{}
kubernetesSecret.Name = onePasswordItem.Name
kubernetesSecret.Namespace = onePasswordItem.Namespace
if err := r.Delete(ctx, kubernetesSecret); err != nil {
if !errors.IsNotFound(err) {
return err
}
}
return nil
}
func (r *OnePasswordItemReconciler) removeOnePasswordFinalizerFromOnePasswordItem(ctx context.Context, onePasswordItem *onepasswordv1.OnePasswordItem) error {
onePasswordItem.Finalizers = utils.RemoveString(onePasswordItem.Finalizers, finalizer)
return r.Update(ctx, onePasswordItem)
}
func (r *OnePasswordItemReconciler) handleOnePasswordItem(ctx context.Context, resource *onepasswordv1.OnePasswordItem, _ ctrl.Request) error {
secretName := resource.GetName()
labels := resource.Labels
secretType := resource.Type
autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation]
item, err := op.GetOnePasswordItemByPath(ctx, r.OpClient, resource.Spec.ItemPath)
if err != nil {
return fmt.Errorf("failed to retrieve item: %w", err)
}
// Create owner reference.
gvk, err := apiutil.GVKForObject(resource, r.Scheme)
if err != nil {
return fmt.Errorf("could not to retrieve group version kind: %w", err)
}
ownerRef := &metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: resource.GetName(),
UID: resource.GetUID(),
}
return kubeSecrets.CreateKubernetesSecretFromItem(ctx, r.Client, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef)
}
func (r *OnePasswordItemReconciler) updateStatus(ctx context.Context, 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.Status().Update(ctx, 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,
}
}