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
1 change: 1 addition & 0 deletions backend/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ func (f *BackendRootCmdFlags) ToBackendOptions(ctx context.Context, cmd *cobra.C
TracerProviderShutdownFunc: otelShutdown,
MaestroSourceEnvironmentIdentifier: f.MaestroSourceEnvironmentIdentifier,
FPAClientBuilder: fpaClientBuilder,
HasRealFPA: !f.InsecureIgnoreUserAzureManagedIdentitiesThatNeedManagedIdentitiesDataplaneAvailableAndUseMock,
BackendIdentityAzureClients: backendIdentityAzureClients,
BackendIdentityAzureCachedReaders: backendIdentityAzureCachedReaders,
ExitOnPanic: f.ExitOnPanic,
Expand Down
56 changes: 39 additions & 17 deletions backend/pkg/app/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
credentialrevocationdeletion "github.com/Azure/ARO-HCP/backend/pkg/controllers/cluster/credentialrevocation/deletion"
credentialrevocationoperations "github.com/Azure/ARO-HCP/backend/pkg/controllers/cluster/credentialrevocation/operations"
clusterdeletion "github.com/Azure/ARO-HCP/backend/pkg/controllers/cluster/deletion"
"github.com/Azure/ARO-HCP/backend/pkg/controllers/cluster/denyassignments"
clusteridentity "github.com/Azure/ARO-HCP/backend/pkg/controllers/cluster/identity"
"github.com/Azure/ARO-HCP/backend/pkg/controllers/cluster/legacycredentialrequest"
clusteroperations "github.com/Azure/ARO-HCP/backend/pkg/controllers/cluster/operations"
Expand Down Expand Up @@ -96,23 +97,27 @@ type Backend struct {
}

type BackendOptions struct {
AppShortDescriptionName string
AppVersion string
AzureLocation string
LeaderElectionLock resourcelock.Interface
ResourcesDBClient corecosmosstorage.ResourcesDBClient
BillingDBClient billingcosmosstorage.BillingDBClient
FleetDBClient fleetcosmosstorage.FleetDBClient
KubeApplierDBClients kubeappliercosmosstorage.KubeApplierDBClients
ClustersServiceClient ocm.ClusterServiceClientSpec
MetricsRegisterer prometheus.Registerer
MetricsGatherer prometheus.Gatherer
MetricsServerListenAddress string
MetricsServerListener net.Listener
HealthzServerListenAddress string
TracerProviderShutdownFunc func(context.Context) error
MaestroSourceEnvironmentIdentifier string
FPAClientBuilder azureclient.FirstPartyApplicationClientBuilder
AppShortDescriptionName string
AppVersion string
AzureLocation string
LeaderElectionLock resourcelock.Interface
ResourcesDBClient corecosmosstorage.ResourcesDBClient
BillingDBClient billingcosmosstorage.BillingDBClient
FleetDBClient fleetcosmosstorage.FleetDBClient
KubeApplierDBClients kubeappliercosmosstorage.KubeApplierDBClients
ClustersServiceClient ocm.ClusterServiceClientSpec
MetricsRegisterer prometheus.Registerer
MetricsGatherer prometheus.Gatherer
MetricsServerListenAddress string
MetricsServerListener net.Listener
HealthzServerListenAddress string
TracerProviderShutdownFunc func(context.Context) error
MaestroSourceEnvironmentIdentifier string
FPAClientBuilder azureclient.FirstPartyApplicationClientBuilder
// HasRealFPA indicates the backend runs against a real First Party Application rather than the
// insecure MI mock. Controllers that create Azure resources only a real FPA can create (e.g.
// deny assignments) are disabled when this is false (dev/int environments).
HasRealFPA bool
BackendIdentityAzureClients *azureclient.BackendIdentityAzureClients
BackendIdentityAzureCachedReaders *cachedreader.BackendIdentityAzureCachedReaders
ExitOnPanic bool
Expand Down Expand Up @@ -951,6 +956,19 @@ func (b *Backend) runBackendControllersUnderLeaderElection(ctx context.Context,
backendInformers,
)

// The deny assignment controller creates Azure deny assignments through the FPA, which only
// exists in environments with a real First Party Application (stage/prod). Skip it entirely when
// running against the MI mock (dev/int), where deny assignments cannot be created.
var clusterDenyAssignmentController controllerutils.Controller
if b.options.HasRealFPA {
clusterDenyAssignmentController = denyassignments.NewClusterDenyAssignmentController(
utilsclock.RealClock{},
b.options.ResourcesDBClient,
b.options.FPAClientBuilder,
backendInformers,
)
}

clusterPendingClusterServiceIDAssignController := clustercreation.NewClusterPendingClusterServiceIDAssignController(
b.options.ResourcesDBClient,
backendInformers,
Expand All @@ -960,6 +978,7 @@ func (b *Backend) runBackendControllersUnderLeaderElection(ctx context.Context,
b.options.ResourcesDBClient,
b.options.ClustersServiceClient,
backendInformers,
b.options.HasRealFPA,
)

clusterDeletionClusterServiceDeleteDispatchController := clusterdeletion.NewClusterClusterServiceDeleteDispatchController(
Expand Down Expand Up @@ -1063,6 +1082,9 @@ func (b *Backend) runBackendControllersUnderLeaderElection(ctx context.Context,
go systemAdminCredentialRevocationDesiresController.Run(ctx, 20)
go systemAdminCredentialRevocationCompletionController.Run(ctx, 20)
go systemAdminCredentialRevocationDeletionController.Run(ctx, 20)
if clusterDenyAssignmentController != nil {
go clusterDenyAssignmentController.Run(ctx, 20)
}
go clusterPendingClusterServiceIDAssignController.Run(ctx, 20)
go clusterClusterServiceCreateController.Run(ctx, 20)
go nodePoolClusterServiceCreateController.Run(ctx, 20)
Expand Down
126 changes: 126 additions & 0 deletions backend/pkg/azure/azuremockclient/mock_clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2026 Microsoft Corporation

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.

We can use mockgen like it's done for other azure clients

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.

We can use mockgen like it's done for other azure clients

came out with ugly test results for pagers.

//
// 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 azuremockclient

import (
"context"
"fmt"

azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"

azureclient "github.com/Azure/ARO-HCP/backend/pkg/azure/client"
)

// DenyAssignmentsClientFunc adapts a function to the DenyAssignmentsClient.Get interface.
// Tests set GetFunc to control the response per call.
type DenyAssignmentsClientFunc struct {
GetFunc func(ctx context.Context, scope string, denyAssignmentID string, options *armauthorization.DenyAssignmentsClientGetOptions) (armauthorization.DenyAssignmentsClientGetResponse, error)
}

var _ azureclient.DenyAssignmentsClient = (*DenyAssignmentsClientFunc)(nil)

func (m *DenyAssignmentsClientFunc) Get(ctx context.Context, scope string, denyAssignmentID string, options *armauthorization.DenyAssignmentsClientGetOptions) (armauthorization.DenyAssignmentsClientGetResponse, error) {
if m.GetFunc != nil {
return m.GetFunc(ctx, scope, denyAssignmentID, options)
}
return armauthorization.DenyAssignmentsClientGetResponse{}, fmt.Errorf("GetFunc not set")
}

func (m *DenyAssignmentsClientFunc) NewListForResourceGroupPager(_ string, _ *armauthorization.DenyAssignmentsClientListForResourceGroupOptions) *azruntime.Pager[armauthorization.DenyAssignmentsClientListForResourceGroupResponse] {
return nil
}

// GenericResourcesClientFunc adapts functions to the GenericResourcesClient interface.
// Tests set the function fields to control the response.
// BeginCreateOrUpdateByID and BeginDeleteByID return a nil *Poller and an error — to simulate
// success, return (nil, nil) and the calling code will call PollUntilDone on nil.
// To avoid that, the tests should exercise paths that don't reach PollUntilDone (e.g. error paths)
// or the mock should capture the call without returning a real poller.
//
// For paths that call PollUntilDone, set CreateErr/DeleteErr to non-nil to prevent the nil-pointer dereference.
type GenericResourcesClientFunc struct {
CreateCalls []GenericResourceCreateCall
DeleteCalls []string
CreateErr error
DeleteErr error
}

type GenericResourceCreateCall struct {
ResourceID string
APIVersion string
Resource armresources.GenericResource
}

var _ azureclient.GenericResourcesClient = (*GenericResourcesClientFunc)(nil)

func (m *GenericResourcesClientFunc) BeginCreateOrUpdateByID(ctx context.Context, resourceID string, apiVersion string, parameters armresources.GenericResource, options *armresources.ClientBeginCreateOrUpdateByIDOptions) (*azruntime.Poller[armresources.ClientCreateOrUpdateByIDResponse], error) {
m.CreateCalls = append(m.CreateCalls, GenericResourceCreateCall{
ResourceID: resourceID,
APIVersion: apiVersion,
Resource: parameters,
})
if m.CreateErr != nil {
return nil, m.CreateErr
}
return nil, fmt.Errorf("GenericResourcesClientFunc: set CreateErr to control this path; PollUntilDone cannot be called on a nil poller")
}

func (m *GenericResourcesClientFunc) BeginDeleteByID(ctx context.Context, resourceID string, apiVersion string, options *armresources.ClientBeginDeleteByIDOptions) (*azruntime.Poller[armresources.ClientDeleteByIDResponse], error) {
m.DeleteCalls = append(m.DeleteCalls, resourceID)
if m.DeleteErr != nil {
return nil, m.DeleteErr
}
return nil, fmt.Errorf("GenericResourcesClientFunc: set DeleteErr to control this path; PollUntilDone cannot be called on a nil poller")
}

// FirstPartyApplicationClientBuilderFunc builds mock Azure clients.
type FirstPartyApplicationClientBuilderFunc struct {
GenericResourcesClientVal azureclient.GenericResourcesClient
GenericResourcesClientErr error
DenyAssignmentsClientVal azureclient.DenyAssignmentsClient
DenyAssignmentsClientErr error
}

var _ azureclient.FirstPartyApplicationClientBuilder = (*FirstPartyApplicationClientBuilderFunc)(nil)

func (m *FirstPartyApplicationClientBuilderFunc) BuilderType() azureclient.FirstPartyApplicationClientBuilderType {
return azureclient.FirstPartyApplicationClientBuilderTypeValue
}

func (m *FirstPartyApplicationClientBuilderFunc) ResourceGroupsClient(tenantID string, subscriptionID string) (azureclient.ResourceGroupsClient, error) {
return nil, fmt.Errorf("not implemented")
}

func (m *FirstPartyApplicationClientBuilderFunc) ResourceProvidersClient(tenantID string, subscriptionID string) (azureclient.ResourceProvidersClient, error) {
return nil, fmt.Errorf("not implemented")
}

func (m *FirstPartyApplicationClientBuilderFunc) ResourceSKUsClient(tenantID string, subscriptionID string) (azureclient.ResourceSKUsClient, error) {
return nil, fmt.Errorf("not implemented")
}

func (m *FirstPartyApplicationClientBuilderFunc) UsageClient(tenantID string, subscriptionID string) (azureclient.UsageClient, error) {
return nil, fmt.Errorf("not implemented")
}

func (m *FirstPartyApplicationClientBuilderFunc) GenericResourcesClient(tenantID string, subscriptionID string) (azureclient.GenericResourcesClient, error) {
return m.GenericResourcesClientVal, m.GenericResourcesClientErr
}

func (m *FirstPartyApplicationClientBuilderFunc) DenyAssignmentsClient(tenantID string, subscriptionID string) (azureclient.DenyAssignmentsClient, error) {
return m.DenyAssignmentsClientVal, m.DenyAssignmentsClientErr
}
29 changes: 29 additions & 0 deletions backend/pkg/azure/client/deny_assignments_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 client

import (
"context"

azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
)

type DenyAssignmentsClient interface {
Get(ctx context.Context, scope string, denyAssignmentID string, options *armauthorization.DenyAssignmentsClientGetOptions) (armauthorization.DenyAssignmentsClientGetResponse, error)
NewListForResourceGroupPager(resourceGroupName string, options *armauthorization.DenyAssignmentsClientListForResourceGroupOptions) *azruntime.Pager[armauthorization.DenyAssignmentsClientListForResourceGroupResponse]
}

var _ DenyAssignmentsClient = (*armauthorization.DenyAssignmentsClient)(nil)
21 changes: 21 additions & 0 deletions backend/pkg/azure/client/fpa_client_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package client

import (
azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"

Expand Down Expand Up @@ -47,6 +48,8 @@ type FirstPartyApplicationClientBuilder interface {
ResourceProvidersClient(tenantID string, subscriptionID string) (ResourceProvidersClient, error)
ResourceSKUsClient(tenantID string, subscriptionID string) (ResourceSKUsClient, error)
UsageClient(tenantID string, subscriptionID string) (UsageClient, error)
GenericResourcesClient(tenantID string, subscriptionID string) (GenericResourcesClient, error)
DenyAssignmentsClient(tenantID string, subscriptionID string) (DenyAssignmentsClient, error)
}

type firstPartyApplicationClientBuilder struct {
Expand Down Expand Up @@ -103,6 +106,24 @@ func (b *firstPartyApplicationClientBuilder) UsageClient(tenantID string, subscr
return armcompute.NewUsageClient(subscriptionID, creds, b.options)
}

func (b *firstPartyApplicationClientBuilder) GenericResourcesClient(tenantID string, subscriptionID string) (GenericResourcesClient, error) {
creds, err := b.fpaTokenCredRetriever.RetrieveCredential(tenantID)
if err != nil {
return nil, err
}

return armresources.NewClient(subscriptionID, creds, b.options)
}

func (b *firstPartyApplicationClientBuilder) DenyAssignmentsClient(tenantID string, subscriptionID string) (DenyAssignmentsClient, error) {
creds, err := b.fpaTokenCredRetriever.RetrieveCredential(tenantID)
if err != nil {
return nil, err
}

return armauthorization.NewDenyAssignmentsClient(subscriptionID, creds, b.options)
}

func (b *firstPartyApplicationClientBuilder) BuilderType() FirstPartyApplicationClientBuilderType {
return FirstPartyApplicationClientBuilderTypeValue
}
29 changes: 29 additions & 0 deletions backend/pkg/azure/client/generic_resources_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 client

import (
"context"

azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
)

type GenericResourcesClient interface {
BeginCreateOrUpdateByID(ctx context.Context, resourceID string, apiVersion string, parameters armresources.GenericResource, options *armresources.ClientBeginCreateOrUpdateByIDOptions) (*azruntime.Poller[armresources.ClientCreateOrUpdateByIDResponse], error)
BeginDeleteByID(ctx context.Context, resourceID string, apiVersion string, options *armresources.ClientBeginDeleteByIDOptions) (*azruntime.Poller[armresources.ClientDeleteByIDResponse], error)
}

var _ GenericResourcesClient = (*armresources.Client)(nil)
Loading