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
30 changes: 21 additions & 9 deletions admin/server/cmd/server/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,25 @@ func (o *ValidatedOptions) Complete(ctx context.Context) (*Options, error) {
}, nil
}

// NewAdminHandler creates an http.Handler for the admin API with all middleware configured.
func NewAdminHandler(
logger *slog.Logger,
dbClient database.DBClient,
csClient ocm.ClusterServiceClientSpec,
fpaCredRetriever fpa.FirstPartyApplicationTokenCredentialRetriever,
) http.Handler {
// Submux for V1 HCP endpoints
v1HCPMux := middleware.NewHCPResourceServerMux()
v1HCPMux.Handle("GET", "/helloworld", hcp.HCPHelloWorld(dbClient, csClient))
v1HCPMux.Handle("GET", "/cosmosdump", cosmosdump.NewCosmosDumpHandler(dbClient))

rootMux := http.NewServeMux()
rootMux.Handle("/admin/helloworld", handlers.HelloWorldHandler())
rootMux.Handle("/admin/v1/hcp/", http.StripPrefix("/admin/v1/hcp", v1HCPMux.Handler()))

return middleware.WithClientPrincipal(middleware.WithLowercaseURLPathValue(middleware.WithLogger(logger, rootMux)))
}

func (opts *Options) Run(ctx context.Context) error {
logger := opts.Logger
logger.Info("Reporting health.", "port", opts.HealthPort)
Expand All @@ -214,18 +233,11 @@ func (opts *Options) Run(ctx context.Context) error {

logger.Info("Running server", "port", opts.Port)

// Submux for V1 HCP endpoints
v1HCPMux := middleware.NewHCPResourceServerMux()
v1HCPMux.Handle("GET", "/helloworld", hcp.HCPHelloWorld(opts.DbClient, opts.ClustersServiceClient, opts.FpaCredentialRetriever))
v1HCPMux.Handle("GET", "/cosmosdump", cosmosdump.NewCosmosDumpHandler(opts.DbClient))

rootMux := http.NewServeMux()
rootMux.Handle("/admin/helloworld", handlers.HelloWorldHandler())
rootMux.Handle("/admin/v1/hcp/", http.StripPrefix("/admin/v1/hcp", v1HCPMux.Handler()))
handler := NewAdminHandler(opts.Logger, opts.DbClient, opts.ClustersServiceClient, opts.FpaCredentialRetriever)

s := http.Server{
Addr: net.JoinHostPort("", strconv.Itoa(opts.Port)),
Handler: middleware.WithClientPrincipal(middleware.WithLowercaseURLPathValue(middleware.WithLogger(logger, rootMux))),
Handler: handler,
}
interrupts.ListenAndServe(&s, 5*time.Second)
interrupts.WaitForGracefulShutdown()
Expand Down
1 change: 0 additions & 1 deletion admin/server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
github.com/Azure/ARO-HCP/internal v0.0.0-00010101000000-000000000000
github.com/Azure/azure-kusto-go v0.16.1
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0
github.com/openshift-online/ocm-sdk-go v0.1.480
github.com/prometheus/client_golang v1.23.2
github.com/spf13/cobra v1.10.2
Expand Down
6 changes: 0 additions & 6 deletions admin/server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ 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/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/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/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/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=
Expand Down
38 changes: 3 additions & 35 deletions admin/server/handlers/hcp/helloworld.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@ import (
"fmt"
"net/http"

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"

"github.com/Azure/ARO-HCP/admin/server/middleware"
"github.com/Azure/ARO-HCP/internal/database"
"github.com/Azure/ARO-HCP/internal/fpa"
"github.com/Azure/ARO-HCP/internal/ocm"
"github.com/Azure/ARO-HCP/internal/utils"
)
Expand All @@ -36,7 +33,7 @@ import (
// in tandem.
//

func HCPHelloWorld(dbClient database.DBClient, csClient ocm.ClusterServiceClientSpec, fpaCredentialRetriever fpa.FirstPartyApplicationTokenCredentialRetriever) http.Handler {
func HCPHelloWorld(dbClient database.DBClient, csClient ocm.ClusterServiceClientSpec) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// get the azure resource ID for this HCP
resourceID, err := utils.ResourceIDFromContext(request.Context())
Expand Down Expand Up @@ -66,43 +63,14 @@ func HCPHelloWorld(dbClient database.DBClient, csClient ocm.ClusterServiceClient
return
}

// get first party application token credentials for the HCP
Copy link
Contributor

Choose a reason for hiding this comment

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

Help me understand what's going on here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Listing the LBs of the managed RG was just for showcasing the FPA creds in this demo handler. I did not want to spend time introducing fakes for the LB azure service at this point if we will not need it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's go in the other direction. Backend wants these creds in @miguelsorianod's PR. Let's actually start an endpoint to pull useful/important azure resources for debugging a cluster.

tokenCredential, err := fpaCredentialRetriever.RetrieveCredential(csCluster.Azure().TenantID())
if err != nil {
http.Error(writer, fmt.Sprintf("failed to get FPA token credentials: %v", err), http.StatusInternalServerError)
return
}

// fetch all loadbalancers from the managedresource group using azuresdk
lbClient, err := armnetwork.NewLoadBalancersClient(csCluster.Azure().SubscriptionID(), tokenCredential, nil)
if err != nil {
http.Error(writer, fmt.Sprintf("failed to create load balancer client: %v", err), http.StatusInternalServerError)
return
}
pager := lbClient.NewListPager(csCluster.Azure().ManagedResourceGroupName(), nil)
var loadBalancers []string
for pager.More() {
page, err := pager.NextPage(request.Context())
if err != nil {
http.Error(writer, fmt.Sprintf("failed to list load balancers: %v", err), http.StatusInternalServerError)
return
}
for _, lb := range page.Value {
if lb.Name != nil {
loadBalancers = append(loadBalancers, *lb.Name)
}
}
}

// some output
output := map[string]any{
"resourceID": resourceID.String(),
"resourceID": hcp.ID.String(),
"internalClusterID": hcp.ServiceProviderProperties.ClusterServiceID,
"clientPrincipalName": clientPrincipalName,
"hcp": hcp,
"tenantID": csCluster.Azure().TenantID(),
"managedResourceGroup": csCluster.Azure().ManagedResourceGroupName(),
"loadBalancers": loadBalancers,
"hcpName": hcp.Name,
}
err = json.NewEncoder(writer).Encode(output)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions test-integration/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ LDFLAGS = -ldflags "\
# Default target
.DEFAULT_GOAL := test

# Build the binary
# Run the integration tests with the Cosmos DB emulator running in a container.
test:
go test github.com/Azure/ARO-HCP/test-int
.PHONY: build
RESTART_EXISTING_EMULATOR=false ./hack/start-cosmos-emulator.sh
./hack/test-integration.sh
.PHONY: test


clean:
.PHONY: clean

69 changes: 69 additions & 0 deletions test-integration/admin/admin_crud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2025 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 admin

import (
"context"
"embed"
"io/fs"
"testing"

"github.com/stretchr/testify/require"

"github.com/Azure/ARO-HCP/internal/api"
"github.com/Azure/ARO-HCP/test-integration/utils/databasemutationhelpers"
"github.com/Azure/ARO-HCP/test-integration/utils/integrationutils"
)

//go:embed artifacts
var artifacts embed.FS

func TestAdminCRUD(t *testing.T) {
integrationutils.SkipIfNotSimulationTesting(t)

ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

allCRUDDirFS, err := fs.Sub(artifacts, "artifacts/AdminCRUD")
require.NoError(t, err)

crudSuiteDirs := api.Must(fs.ReadDir(allCRUDDirFS, "."))
for _, crudSuiteDirEntry := range crudSuiteDirs {
crudSuiteDir := api.Must(fs.Sub(allCRUDDirFS, crudSuiteDirEntry.Name()))
t.Run(crudSuiteDirEntry.Name(), func(t *testing.T) {
testAdminCRUDSuite(
ctx,
t,
crudSuiteDir)
})
}
}

func testAdminCRUDSuite(ctx context.Context, t *testing.T, crudSuiteDir fs.FS) {
testDirs := api.Must(fs.ReadDir(crudSuiteDir, "."))
for _, testDirEntry := range testDirs {
testDir := api.Must(fs.Sub(crudSuiteDir, testDirEntry.Name()))

currTest, err := databasemutationhelpers.NewAdminResourceMutationTest(
ctx,
testDirEntry.Name(),
testDir,
)
require.NoError(t, err)

t.Run(testDirEntry.Name(), currTest.RunTest)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"id": "|subscriptions|0465bc32-c654-41b8-8d87-9815d7abe8f6|resourcegroups|some-resource-group|providers|microsoft.redhatopenshift|hcpopenshiftclusters|some-hcp-cluster",
"partitionKey": "0465bc32-c654-41b8-8d87-9815d7abe8f6",
"resourceType": "Microsoft.RedHatOpenShift/hcpOpenShiftClusters",
"properties": {
"internalId": "/api/aro_hcp/v1alpha1/clusters/fixed-value",
"identity": {
"type": "UserAssigned"
},
"internalState": {
"internalAPI": {
"location": "eastus",
"serviceProviderProperties": {
"clusterServiceID": ""
}
}
},
"provisioningState": "Succeeded",
"resourceId": "/subscriptions/0465bc32-c654-41b8-8d87-9815d7abe8f6/resourceGroups/some-resource-group/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/some-hcp-cluster",
"systemData": {},
"tags": {
"foo": "bar"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"api": {
"cidr_block_access": {
"allow": {
"mode": "allow_all"
}
},
"listening": "external"
},
"azure": {
"etcd_encryption": {
"data_encryption": {
"customer_managed": {
"encryption_type": "kms",
"kms": {
"active_key": {
"key_name": "encryptionKeyName",
"key_vault_name": "keyVaultName",
"key_version": "2024-12-01-preview"
}
}
},
"key_management_mode": "customer_managed"
}
},
"managed_resource_group_name": "fake-mrg-name",
"network_security_group_resource_id": "/subscriptions/6b690bec-0c16-4ecb-8f67-781caf40bba7/resourceGroups/bar/providers/Microsoft.Network/networkSecurityGroups/nsg",
"nodes_outbound_connectivity": {
"outbound_type": "load_balancer"
},
"operators_authentication": {
"managed_identities": {
"control_plane_operators_managed_identities": {},
"data_plane_operators_managed_identities": {},
"managed_identities_data_plane_identity_url": ""
}
},
"resource_group_name": "resourcegroupname",
"resource_name": "create-with-tags",
"subnet_resource_id": "/subscriptions/6b690bec-0c16-4ecb-8f67-781caf40bba7/resourceGroups/bar/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet",
"subscription_id": "6b690bec-0c16-4ecb-8f67-781caf40bba7",
"tenant_id": "fake-tenant-id"
},
"ccs": {
"enabled": true,
"kind": "CCS"
},
"cloud_provider": {
"id": "azure",
"kind": "CloudProvider"
},
"flavour": {
"id": "osd-4",
"kind": "Flavour"
},
"href": "/api/aro_hcp/v1alpha1/clusters/fixed-value",
"hypershift": {
"enabled": true
},
"id": "fixed-value",
"image_registry": {
"state": "disabled"
},
"kind": "Cluster",
"name": "create-with-tags",
"network": {
"host_prefix": 23,
"machine_cidr": "10.0.0.0/16",
"pod_cidr": "10.128.0.0/14",
"service_cidr": "172.30.0.0/16",
"type": "OVNKubernetes"
},
"node_drain_grace_period": {
"unit": "minutes",
"value": 0
},
"product": {
"id": "aro",
"kind": "Product"
},
"region": {
"id": "fake-location",
"kind": "CloudRegion"
},
"version": {
"channel_group": "stable",
"id": "",
"kind": "Version"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"resourceID": "/admin/v1/hcp/subscriptions/0465bc32-c654-41b8-8d87-9815d7abe8f6/resourceGroups/some-resource-group/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/some-hcp-cluster/helloworld"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"resourceID": "/subscriptions/0465bc32-c654-41b8-8d87-9815d7abe8f6/resourceGroups/some-resource-group/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/some-hcp-cluster",
"internalClusterID": "/api/aro_hcp/v1alpha1/clusters/fixed-value",
"clientPrincipalName": "test-user@example.com",
"tenantID": "fake-tenant-id",
"managedResourceGroup": "fake-mrg-name",
"hcpName": "some-hcp-cluster"
}
2 changes: 1 addition & 1 deletion test-integration/frontend/cluster_mutation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (tt *clusterMutationTest) runTest(t *testing.T) {

toCreate := &hcpsdk20240610preview.HcpOpenShiftCluster{}
require.NoError(t, json.Unmarshal(tt.genericMutationTestInfo.CreateJSON, toCreate))
clusterClient := tt.testInfo.Get20240610ClientFactory(tt.subscriptionID).NewHcpOpenShiftClustersClient()
clusterClient := integrationutils.Get20240610ClientFactory(tt.testInfo.FrontendURL, tt.subscriptionID).NewHcpOpenShiftClustersClient()
_, mutationErr := clusterClient.BeginCreateOrUpdate(ctx, tt.resourceGroupName, *toCreate.Name, *toCreate, nil)

if tt.genericMutationTestInfo.IsUpdateTest() || tt.genericMutationTestInfo.IsPatchTest() {
Expand Down
4 changes: 2 additions & 2 deletions test-integration/frontend/cluster_read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestFrontendClusterRead(t *testing.T) {

resourceGroup := "some-resource-group"
hcpClusterName := "some-hcp-cluster"
hcpCluster, err := testInfo.Get20240610ClientFactory(subscriptionID).NewHcpOpenShiftClustersClient().Get(ctx, resourceGroup, hcpClusterName, nil)
hcpCluster, err := integrationutils.Get20240610ClientFactory(testInfo.FrontendURL, subscriptionID).NewHcpOpenShiftClustersClient().Get(ctx, resourceGroup, hcpClusterName, nil)
require.NoError(t, err)

actualJSON, err := json.MarshalIndent(hcpCluster, "", " ")
Expand All @@ -66,7 +66,7 @@ func TestFrontendClusterRead(t *testing.T) {
require.NoError(t, json.Unmarshal(api.Must(artifacts.ReadFile("artifacts/ClusterReadOldData/some-hcp-cluster--expected.json")), &expectedMap))
require.Equal(t, expectedMap, actualMap)

_, err = testInfo.Get20240610ClientFactory(subscriptionID).NewHcpOpenShiftClustersClient().BeginDelete(ctx, resourceGroup, hcpClusterName, nil)
_, err = integrationutils.Get20240610ClientFactory(testInfo.FrontendURL, subscriptionID).NewHcpOpenShiftClustersClient().BeginDelete(ctx, resourceGroup, hcpClusterName, nil)
require.NoError(t, err)
// the poller will never be done because we aren't running the backend. Just let it be.
}
2 changes: 1 addition & 1 deletion test-integration/frontend/externalauth_mutation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (tt *externalAuthMutationTest) runTest(t *testing.T) {
hcpClusterName := strings.Split(t.Name(), "/")[1]
toCreate := &hcpsdk20240610preview.ExternalAuth{}
require.NoError(t, json.Unmarshal(tt.genericMutationTestInfo.CreateJSON, toCreate))
externalAuthClient := tt.testInfo.Get20240610ClientFactory(tt.subscriptionID).NewExternalAuthsClient()
externalAuthClient := integrationutils.Get20240610ClientFactory(tt.testInfo.FrontendURL, tt.subscriptionID).NewExternalAuthsClient()
_, mutationErr := externalAuthClient.BeginCreateOrUpdate(ctx, tt.resourceGroupName, hcpClusterName, *toCreate.Name, *toCreate, nil)

if tt.genericMutationTestInfo.IsUpdateTest() || tt.genericMutationTestInfo.IsPatchTest() {
Expand Down
2 changes: 1 addition & 1 deletion test-integration/frontend/nodepool_mutation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (tt *nodePoolMutationTest) runTest(t *testing.T) {
hcpClusterName := strings.Split(t.Name(), "/")[1]
toCreate := &hcpsdk20240610preview.NodePool{}
require.NoError(t, json.Unmarshal(tt.genericMutationTestInfo.CreateJSON, toCreate))
nodePoolClient := tt.testInfo.Get20240610ClientFactory(tt.subscriptionID).NewNodePoolsClient()
nodePoolClient := integrationutils.Get20240610ClientFactory(tt.testInfo.FrontendURL, tt.subscriptionID).NewNodePoolsClient()
_, mutationErr := nodePoolClient.BeginCreateOrUpdate(ctx, tt.resourceGroupName, hcpClusterName, *toCreate.Name, *toCreate, nil)

if tt.genericMutationTestInfo.IsUpdateTest() || tt.genericMutationTestInfo.IsPatchTest() {
Expand Down
Loading