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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ linters:
alias: $1$2
- pkg: github.com/Azure/ARO-HCP/test/sdk/v20240610preview/resourcemanager/redhatopenshifthcp/armredhatopenshifthcp"
alias: hcpsdk20240610preview
- pkg: github.com/Azure/ARO-HCP/backend/pkg/azure/client
alias: azureclient
staticcheck:
dot-import-whitelist:
- "github.com/onsi/ginkgo"
Expand Down
87 changes: 87 additions & 0 deletions backend/azure_config_wiring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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 main

import (
"context"
"fmt"
"os"

"go.opentelemetry.io/otel/trace"

k8soperation "k8s.io/apimachinery/pkg/api/operation"

"sigs.k8s.io/yaml"

apisconfigv1 "github.com/Azure/ARO-HCP/backend/pkg/apis/config/v1"
azureconfig "github.com/Azure/ARO-HCP/backend/pkg/azure/config"
)

func loadAzureRuntimeConfig(ctx context.Context, path string) (*apisconfigv1.AzureRuntimeConfig, error) {
if len(path) == 0 {
return nil, fmt.Errorf("configuration path is required")
}

rawBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading file %s: %w", path, err)
}

var config apisconfigv1.AzureRuntimeConfig
err = yaml.Unmarshal(rawBytes, &config)
if err != nil {
return nil, fmt.Errorf("error unmarshaling file %s: %w", path, err)
}

validationErrors := config.Validate(ctx, k8soperation.Operation{Type: k8soperation.Create})
if len(validationErrors) > 0 {
return nil,
fmt.Errorf("error validating file: %s: %w", path, validationErrors.ToAggregate())
}

return &config, nil
}

func buildAzureConfig(azureRuntimeConfig *apisconfigv1.AzureRuntimeConfig, tracerProvider trace.TracerProvider) (*azureconfig.AzureConfig, error) {
cloudEnvironment, err := azureconfig.NewAzureCloudEnvironment(azureRuntimeConfig.CloudEnvironmentName, tracerProvider)
if err != nil {
return nil, fmt.Errorf("error building azure cloud environment configuration: %w", err)
}

out := &azureconfig.AzureConfig{
CloudEnvironment: cloudEnvironment,
AzureRuntimeConfig: azureRuntimeConfig,
}

return out, err
}

func getAzureConfig(ctx context.Context, azureRuntimeConfigPath string, tracerProvider trace.TracerProvider) (*azureconfig.AzureConfig, error) {
if len(azureRuntimeConfigPath) == 0 {
return nil, nil
}

azureRuntimeConfig, err := loadAzureRuntimeConfig(ctx, azureRuntimeConfigPath)
if err != nil {
return nil, fmt.Errorf("error loading azure runtime config: %w", err)
}

azureConfig, err := buildAzureConfig(azureRuntimeConfig, tracerProvider)
if err != nil {
return nil, fmt.Errorf("error building azure configuration: %w", err)
}

return azureConfig, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package controllers

import (
"context"
"fmt"

azureclient "github.com/Azure/ARO-HCP/backend/pkg/azure/client"
"github.com/Azure/ARO-HCP/internal/api"
"github.com/Azure/ARO-HCP/internal/api/arm"
"github.com/Azure/ARO-HCP/internal/utils"
)

// AzureHCPClusterResourceGroupExistenceValidation validates that the Azure Resource
// Group part of the HCP Cluster Resource ID being created exists beforehand.
type AzureHCPClusterResourceGroupExistenceValidation struct {
azureFPAClientBuilder azureclient.FPAClientBuilder
}

func NewAzureHCPClusterResourceGroupExistenceValidation(
azureFPAClientBuilder azureclient.FPAClientBuilder,
) *AzureHCPClusterResourceGroupExistenceValidation {
return &AzureHCPClusterResourceGroupExistenceValidation{
azureFPAClientBuilder: azureFPAClientBuilder,
}
}

func (a *AzureHCPClusterResourceGroupExistenceValidation) Name() string {
return "azure-cluster-resource-group-existence-validation"
}

func (a *AzureHCPClusterResourceGroupExistenceValidation) Validate(
ctx context.Context, clusterSubscription *arm.Subscription, cluster *api.HCPOpenShiftCluster,
) error {
rgClient, err := a.azureFPAClientBuilder.ResourceGroupsClient(
*clusterSubscription.Properties.TenantId,
cluster.ID.SubscriptionID,
)
if err != nil {
return utils.TrackError(fmt.Errorf("failed to get resource groups client: %w", err))
}

_, err = rgClient.Get(ctx, cluster.ID.ResourceGroupName, nil)
if azureclient.IsResourceGroupNotFoundErr(err) {
return utils.TrackError(fmt.Errorf("resource group does not exist: %w", err))
}

if err != nil {
return utils.TrackError(fmt.Errorf("failed to get resource group: %w", err))
}

return nil
}
131 changes: 131 additions & 0 deletions backend/fpa_wiring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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 main

import (
"context"
"encoding/base64"
"fmt"
"log/slog"
"os"
"time"

"github.com/go-logr/logr"

azureclient "github.com/Azure/ARO-HCP/backend/pkg/azure/client"
azureconfig "github.com/Azure/ARO-HCP/backend/pkg/azure/config"
"github.com/Azure/ARO-HCP/internal/fpa"
"github.com/Azure/ARO-HCP/internal/utils"
)

func getFirstPartyApplicationTokenCredentialRetriever(
ctx context.Context, fpaCertBundlePath string,
fpaClientID string, azureConfig *azureconfig.AzureConfig,
) (fpa.FirstPartyApplicationTokenCredentialRetriever, error) {
if len(fpaCertBundlePath) == 0 || len(fpaClientID) == 0 {
return nil, nil
}

// TODO temporary until internal FPA types have been updated to
// use logr.Logger or just receiving from context.
logrLogger := utils.LoggerFromContext(ctx)
slogLogger := slog.New(logr.ToSlogHandler(logrLogger))

// Create FPA TokenCredentials with watching
certReader, err := fpa.NewWatchingFileCertificateReader(
ctx,
fpaCertBundlePath,
1*time.Minute,
slogLogger,
)
if err != nil {
return nil, fmt.Errorf("failed to create certificate reader: %w", err)
}

// We create the FPA token credential retriever here. Then we pass it to the cluster inflights controller,
// which then is used to instantiate a validation that uses the FPA token credential retriever. And then the
// validations uses the retriever to retrieve a token credential based on the information associated to the
// cluster(the tenant of the cluster, the subscription id, ...)
fpaTokenCredRetriever, err := fpa.NewFirstPartyApplicationTokenCredentialRetriever(
slogLogger,
fpaClientID,
certReader,
*azureConfig.CloudEnvironment.AZCoreClientOptions(),
)
if err != nil {
return nil, fmt.Errorf("failed to create FPA token credential retriever: %w", err)
}

return fpaTokenCredRetriever, nil
}

func getFirstPartyApplicationClientBuilder(
fpaTokenCredRetriever fpa.FirstPartyApplicationTokenCredentialRetriever, azureConfig *azureconfig.AzureConfig,
) (azureclient.FirstPartyApplicationClientBuilder, error) {
fpaClientBuilder := azureclient.NewFirstPartyApplicationClientBuilder(
fpaTokenCredRetriever, azureConfig.CloudEnvironment.ARMClientOptions(),
)

return fpaClientBuilder, nil
}

func getFirstPartyApplicationManagedIdentitiesDataplaneClientBuilder(
fpaTokenCredRetriever fpa.FirstPartyApplicationTokenCredentialRetriever,
azureMIMockSPCertBundlePath string, azureMIMockSPClientID string, azureMIMockSPPrincipalID string, azureMIMockSPTenantID string,
azureConfig *azureconfig.AzureConfig,
) (azureclient.FPAMIDataplaneClientBuilder, error) {

if len(azureMIMockSPCertBundlePath) == 0 || len(azureMIMockSPClientID) == 0 || len(azureMIMockSPPrincipalID) == 0 {
// TODO if we want to support detecting when the cert bundle path content
// changes, we could use a file watcher similar to the one used in the
// fpa token credential retriever, and pass that retriever to the client
// builder.
bundle, err := os.ReadFile(azureMIMockSPCertBundlePath)
if err != nil {
return nil, fmt.Errorf("failed to read bundle file: %w", err)
}
bundleBase64Encoded := base64.StdEncoding.EncodeToString(bundle)
hardcodedIdentity := &azureclient.HardcodedIdentity{
ClientID: azureMIMockSPClientID,
ClientSecret: bundleBase64Encoded,
PrincipalID: azureMIMockSPPrincipalID,
TenantID: azureMIMockSPTenantID,
}
hardcodedIdentityFPAMIDataplaneClientBuilder := azureclient.NewHardcodedIdentityFPAMIDataplaneClientBuilder(
azureConfig.CloudEnvironment.CloudConfiguration(),
hardcodedIdentity,
)
return hardcodedIdentityFPAMIDataplaneClientBuilder, nil
}

fpaMIdataplaneClientBuilder := azureclient.NewFPAMIDataplaneClientBuilder(
azureConfig.AzureRuntimeConfig.ServiceTenantID,
fpaTokenCredRetriever,
azureConfig.AzureRuntimeConfig.ManagedIdentitiesDataPlaneAudienceResource,
azureConfig.CloudEnvironment.AZCoreClientOptions(),
)

return fpaMIdataplaneClientBuilder, nil
}

func getServiceManagedIdentityClientBuilderFactory(
fpaMIdataplaneClientBuilder azureclient.FPAMIDataplaneClientBuilder,
azureConfig *azureconfig.AzureConfig,
) azureclient.ServiceManagedIdentityClientBuilderFactory {
return azureclient.NewServiceManagedIdentityClientBuilderFactory(
fpaMIdataplaneClientBuilder,
azureConfig.CloudEnvironment.ARMClientOptions(),
)
}
12 changes: 10 additions & 2 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ require (
github.com/Azure/ARO-HCP/internal v0.0.0-00010101000000-000000000000
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel v0.4.0
github.com/Azure/msi-dataplane v0.4.3
github.com/go-logr/logr v1.4.3
github.com/openshift-online/ocm-sdk-go v0.1.480
github.com/prometheus/client_golang v1.23.2
Expand All @@ -21,15 +24,19 @@ require (
k8s.io/client-go v0.34.1
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
sigs.k8s.io/yaml v1.6.0
)

require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
Expand All @@ -39,6 +46,7 @@ require (
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
Expand Down Expand Up @@ -98,6 +106,7 @@ require (
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.39.0 // indirect
Expand All @@ -116,7 +125,6 @@ require (
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

replace github.com/Azure/ARO-HCP/internal => ../internal
20 changes: 20 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,30 @@ 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.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
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/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
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/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
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/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
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 All @@ -43,6 +59,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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand Down Expand Up @@ -242,6 +260,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand Down
Loading