Skip to content
Closed
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
8 changes: 8 additions & 0 deletions backend/pkg/app/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,13 @@ func (b *Backend) runBackendControllersUnderLeaderElection(ctx context.Context,
backendInformers,
)

fetchDataPlaneOperatorsManagedIdentitiesInfoController := controllers.NewFetchDataPlaneOperatorsManagedIdentitiesInfoController(
b.options.CosmosDBClient,
activeOperationLister,
backendInformers,
b.options.SMIClientBuilder,
)

le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
Lock: b.options.LeaderElectionLock,
LeaseDuration: leaderElectionLeaseDuration,
Expand Down Expand Up @@ -527,6 +534,7 @@ func (b *Backend) runBackendControllersUnderLeaderElection(ctx context.Context,
go maestroReadAndPersistReadonlyBundlesContentController.Run(ctx, 20)
go maestroDeleteOrphanedReadonlyBundlesController.Run(ctx, 20)
go triggerNodePoolUpgradeController.Run(ctx, 20)
go fetchDataPlaneOperatorsManagedIdentitiesInfoController.Run(ctx, 20)
},
OnStoppedLeading: func() {
// This needs to be defined even though it does nothing.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Copyright 2026 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers

import (
"context"
"errors"
"fmt"
"net/http"
"time"

"k8s.io/apimachinery/pkg/api/equality"

azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"

azureclient "github.com/Azure/ARO-HCP/backend/pkg/azure/client"
"github.com/Azure/ARO-HCP/backend/pkg/controllers/controllerutils"
"github.com/Azure/ARO-HCP/backend/pkg/informers"
"github.com/Azure/ARO-HCP/backend/pkg/listers"
"github.com/Azure/ARO-HCP/internal/api"
"github.com/Azure/ARO-HCP/internal/database"
"github.com/Azure/ARO-HCP/internal/utils"
)

// fetchDataPlaneOperatorsManagedIdentitiesInfoSyncer is a controller that
// fetches the Client ID and Principal ID of the data plane operators managed identities
// associated to the cluster and stores them in Cosmos.
type fetchDataPlaneOperatorsManagedIdentitiesInfoSyncer struct {
cooldownChecker controllerutils.CooldownChecker

cosmosClient database.DBClient

smiClientBuilder azureclient.ServiceManagedIdentityClientBuilder
}

var _ controllerutils.ClusterSyncer = (*fetchDataPlaneOperatorsManagedIdentitiesInfoSyncer)(nil)

func NewFetchDataPlaneOperatorsManagedIdentitiesInfoController(
cosmosClient database.DBClient,
activeOperationLister listers.ActiveOperationLister,
backendInformers informers.BackendInformers,
smiClientBuilder azureclient.ServiceManagedIdentityClientBuilder,
) controllerutils.Controller {

syncer := &fetchDataPlaneOperatorsManagedIdentitiesInfoSyncer{
cooldownChecker: controllerutils.DefaultActiveOperationPrioritizingCooldown(activeOperationLister),
cosmosClient: cosmosClient,
smiClientBuilder: smiClientBuilder,
}

controller := controllerutils.NewClusterWatchingController(
"FetchDataPlaneOperatorsManagedIdentitiesInfo",
cosmosClient,
backendInformers,
1*time.Minute,
syncer,
)

return controller
}

func (c *fetchDataPlaneOperatorsManagedIdentitiesInfoSyncer) SyncOnce(ctx context.Context, key controllerutils.HCPClusterKey) error {
existingCluster, err := c.cosmosClient.HCPClusters(key.SubscriptionID, key.ResourceGroupName).Get(ctx, key.HCPClusterName)
if database.IsResponseError(err, http.StatusNotFound) {
return nil // cluster doesn't exist, no work to do
}
if err != nil {
return utils.TrackError(fmt.Errorf("failed to get Cluster: %w", err))
}

// TODO unclear if we should put the data plane operators managed identities info in the ServiceProviderCluster resource or in
// the HCPCluster resource. For now we put it in the ServiceProviderCluster resource.
existingServiceProviderCluster, err := controllerutils.GetOrCreateServiceProviderCluster(ctx, c.cosmosClient, key.GetResourceID())

Check failure on line 84 in backend/pkg/controllers/fetch_data_plane_operators_managed_identities_info.go

View workflow job for this annotation

GitHub Actions / Analyze (go)

undefined: controllerutils.GetOrCreateServiceProviderCluster
if err != nil {
return utils.TrackError(fmt.Errorf("failed to get or create ServiceProviderCluster: %w", err))
}

type identityToSync struct {
ResourceID *azcorearm.ResourceID
OperatorName string
}

identitiesToSync := []*identityToSync{}
for operatorName, dataPlaneOperatorResourceID := range existingCluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators {
currentMI, ok := existingServiceProviderCluster.Status.DataPlaneOperatorsManagedIdentities[dataPlaneOperatorResourceID.String()]
if !ok {
identitiesToSync = append(identitiesToSync, &identityToSync{
ResourceID: dataPlaneOperatorResourceID,
OperatorName: operatorName,
})
continue
}

if len(currentMI.ClientID) == 0 || len(currentMI.PrincipalID) == 0 || len(currentMI.OperatorName) == 0 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For when we allow updates of MI; we'll need to also sync the identities if currentMI info != newUpdatedInfo.

identitiesToSync = append(identitiesToSync, &identityToSync{
ResourceID: dataPlaneOperatorResourceID,
OperatorName: operatorName,
})
continue
}

}
if len(identitiesToSync) == 0 {
return nil
}

smiResourceID := existingCluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.ServiceManagedIdentity
uaisClient, err := c.smiClientBuilder.UserAssignedIdentitiesClient(ctx, existingCluster.ServiceProviderProperties.ManagedIdentitiesDataPlaneIdentityURL, smiResourceID, existingCluster.ID.SubscriptionID)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
uaisClient, err := c.smiClientBuilder.UserAssignedIdentitiesClient(ctx, existingCluster.ServiceProviderProperties.ManagedIdentitiesDataPlaneIdentityURL, smiResourceID, existingCluster.ID.SubscriptionID)
userAssignedIdentitiesClient, err := c.smiClientBuilder.UserAssignedIdentitiesClient(ctx, existingCluster.ServiceProviderProperties.ManagedIdentitiesDataPlaneIdentityURL, smiResourceID, existingCluster.ID.SubscriptionID)

don't be stingy.

if err != nil {
return utils.TrackError(fmt.Errorf("failed to get User Assigned Identities Client: %w", err))
}

desiredDataPlaneOperatorsManagedIdentities := make(map[string]*api.ServiceProviderClusterDataPlaneOperatorManagedIdentity)

var syncErrors []error
for _, dataPlaneOperatorIdentityToSync := range identitiesToSync {
desiredDataPlaneOperatorsManagedIdentities[dataPlaneOperatorIdentityToSync.ResourceID.String()] = &api.ServiceProviderClusterDataPlaneOperatorManagedIdentity{
ResourceID: dataPlaneOperatorIdentityToSync.ResourceID,
OperatorName: dataPlaneOperatorIdentityToSync.OperatorName,
}
currentMI, err := uaisClient.Get(ctx, dataPlaneOperatorIdentityToSync.ResourceID.ResourceGroupName, dataPlaneOperatorIdentityToSync.ResourceID.Name, nil)
if err != nil {
syncErrors = append(syncErrors, utils.TrackError(fmt.Errorf("failed to get Data Plane Operator Managed Identity: %w", err)))
continue
}

if currentMI.Properties == nil {
syncErrors = append(syncErrors, utils.TrackError(fmt.Errorf("unexpected Data Plane Operator Managed Identity %s Properties is nil", dataPlaneOperatorIdentityToSync.ResourceID.String())))
continue
}

if currentMI.Properties.ClientID != nil && len(*currentMI.Properties.ClientID) > 0 {
desiredDataPlaneOperatorsManagedIdentities[dataPlaneOperatorIdentityToSync.ResourceID.String()].ClientID = *currentMI.Properties.ClientID
} else {
syncErrors = append(syncErrors, utils.TrackError(fmt.Errorf("unexpected Data Plane Operator Managed Identity %s Client ID is nil or empty", dataPlaneOperatorIdentityToSync.ResourceID.String())))
}

if currentMI.Properties.PrincipalID != nil && len(*currentMI.Properties.PrincipalID) > 0 {
desiredDataPlaneOperatorsManagedIdentities[dataPlaneOperatorIdentityToSync.ResourceID.String()].PrincipalID = *currentMI.Properties.PrincipalID
} else {
syncErrors = append(syncErrors, utils.TrackError(fmt.Errorf("unexpected Data Plane Operator Managed Identity %s Principal ID is nil or empty", dataPlaneOperatorIdentityToSync.ResourceID.String())))
}
}

if !equality.Semantic.DeepEqual(existingServiceProviderCluster.Status.DataPlaneOperatorsManagedIdentities, desiredDataPlaneOperatorsManagedIdentities) {
if existingServiceProviderCluster.Status.DataPlaneOperatorsManagedIdentities == nil {
existingServiceProviderCluster.Status.DataPlaneOperatorsManagedIdentities = make(map[string]*api.ServiceProviderClusterDataPlaneOperatorManagedIdentity)
}

for _, desired := range desiredDataPlaneOperatorsManagedIdentities {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as written, entries created here can never be cleared. An "add only" map smells wrong. What clears unnecessary or extra entries over time.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we need to ensure taht serviceprovidercluster should have its entries trimmed of any that don't exist in existingCluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators

key := desired.ResourceID.String()
entry := existingServiceProviderCluster.Status.DataPlaneOperatorsManagedIdentities[key]
if entry == nil {
entry = &api.ServiceProviderClusterDataPlaneOperatorManagedIdentity{}
existingServiceProviderCluster.Status.DataPlaneOperatorsManagedIdentities[key] = entry
}
entry.ResourceID = desired.ResourceID
entry.OperatorName = desired.OperatorName
if len(desired.ClientID) > 0 {
entry.ClientID = desired.ClientID
}
if len(desired.PrincipalID) > 0 {
entry.PrincipalID = desired.PrincipalID
}
}

_, err := c.cosmosClient.ServiceProviderClusters(existingCluster.ID.SubscriptionID, existingCluster.ID.ResourceGroupName, existingCluster.ID.Name).Replace(ctx, existingServiceProviderCluster, nil)
if err != nil {
syncErrors = append(syncErrors, utils.TrackError(fmt.Errorf("failed to replace ServiceProviderCluster: %w", err)))
}
}

return errors.Join(syncErrors...)
}

func (c *fetchDataPlaneOperatorsManagedIdentitiesInfoSyncer) CooldownChecker() controllerutils.CooldownChecker {
return c.cooldownChecker
}
13 changes: 13 additions & 0 deletions internal/api/types_serviceprovider_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ type ServiceProviderClusterStatus struct {
// The reference contains a mapping between the logical name we give to the Maestro bundle internally
// and the Maestro Bundle Name and ID at the Maestro API level.
MaestroReadonlyBundles MaestroBundleReferenceList `json:"maestroReadonlyBundles,omitempty"`

// DataPlaneOperatorsManagedIdentities is a map of data plane operator managed identities.
// The key is the Azure Resource ID of the managed identity
// TODO do we want the key to be the operator name or the Azure Resource ID?
// TODO do we want to store both the operator name and the Azure Resource ID?
DataPlaneOperatorsManagedIdentities map[string]*ServiceProviderClusterDataPlaneOperatorManagedIdentity `json:"dataPlaneOperatorsManagedIdentities,omitempty"`
}

type ServiceProviderClusterDataPlaneOperatorManagedIdentity struct {
OperatorName string `json:"operatorName"`
ResourceID *azcorearm.ResourceID `json:"resourceID"`
ClientID string `json:"clientID"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be missing. pointer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using the empty string as the missing indicator. In that way there are no three different sets of values to have to consider: nil, empty string, non empty string

PrincipalID string `json:"principalID"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be missing. pointer.

}

// ServiceProviderClusterStatusVersion contains the actual version information.
Expand Down
8 changes: 8 additions & 0 deletions test-integration/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ require (
require (
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-kusto-go v0.16.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeployments v0.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 // indirect
github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel v0.4.0 // indirect
github.com/Azure/msi-dataplane v0.4.3 // indirect
github.com/Azure/retry v0.0.0-20250221010952-92c9290cea0f // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
Expand All @@ -40,6 +47,7 @@ require (
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getsentry/sentry-go v0.20.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect
Expand Down
20 changes: 20 additions & 0 deletions test-integration/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1 h1:ToPLhnXvatKVN4Zkcx
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1/go.mod h1:Krtog/7tz27z75TwM5cIS8bxEH4dcBUezcq+kGVeZEo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.1 h1:1kpY4qe+BGAH2ykv4baVSqyx+AY5VjXeJ15SldlU6hs=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.1/go.mod h1:nT6cWpWdUt+g81yuKmjeYPUtI73Ak3yQIT4PVVsCEEQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.2.0 h1:akP6VpxJGgQRpDR1P462piz/8OhYLRCreDj48AyNabc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.2.0/go.mod h1:8wzvopPfyZYPaQUoKW87Zfdul7jmJMDfp/k7YY3oJyA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3dExHBgUxsO3qpTGhk/P2dgnYyW48yn7AO33Tbek=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 h1:HYGD75g0bQ3VO/Omedm54v4LrD3B1cGImuRF3AJ5wLo=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0/go.mod h1:ulHyBFJOI0ONiRL4vcJTmS7rx18jQQlEPmAgo80cRdM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeployments v0.2.0 h1:bYq3jfB2x36hslKMHyge3+esWzROtJNk/4dCjsKlrl4=
Expand All @@ -26,8 +32,18 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v3 v3.0.1 h1:guyQA4b8XB2sbJZXzUnOF9mn0WDBv/ZT7me9wTipKtE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v3 v3.0.1/go.mod h1:8h8yhzh9o+0HeSIhUxYny+rEQajScrfIpNktvgYG3Q8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel v0.4.0 h1:RTTsXUJWn0jumeX62Mb153wYXykqnrzYBYDeHp0kiuk=
github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel v0.4.0/go.mod h1:k4MMjrPHIEK+umaMGk1GNLgjEybJZ9mHSRDZ+sDFv3Y=
github.com/Azure/msi-dataplane v0.4.3 h1:dWPWzY4b54tLIR9T1Q014Xxd/1DxOsMIp6EjRFAJlQY=
github.com/Azure/msi-dataplane v0.4.3/go.mod h1:yAfxdJyvcnvSDfSyOFV9qm4fReEQDl+nZLGeH2ZWSmw=
github.com/Azure/retry v0.0.0-20250221010952-92c9290cea0f h1:XjKfallhRhddiRmBG0u2gs+Rd75QjvAlPVDy3ZWLjPg=
github.com/Azure/retry v0.0.0-20250221010952-92c9290cea0f/go.mod h1:4FpEaBWwrdI8kVPeNESpqzIYAZipu7K6MCGCCC6bJ/A=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
Expand All @@ -36,6 +52,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgv
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand Down Expand Up @@ -66,6 +84,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhjHmQaQ=
Expand Down
Loading