frontend/cmd/cmd.go (153 lines of code) (raw):
// 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 cmd
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel"
sdk "github.com/openshift-online/ocm-sdk-go"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/cobra"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
"github.com/Azure/ARO-HCP/frontend/pkg/frontend"
"github.com/Azure/ARO-HCP/frontend/pkg/util"
"github.com/Azure/ARO-HCP/internal/api"
"github.com/Azure/ARO-HCP/internal/api/arm"
"github.com/Azure/ARO-HCP/internal/database"
"github.com/Azure/ARO-HCP/internal/ocm"
"github.com/Azure/ARO-HCP/internal/version"
)
type FrontendOpts struct {
clustersServiceURL string
clusterServiceProvisionShard string
clusterServiceNoopProvision bool
clusterServiceNoopDeprovision bool
insecure bool
location string
metricsPort int
port int
cosmosName string
cosmosURL string
}
func NewRootCmd() *cobra.Command {
opts := &FrontendOpts{}
rootCmd := &cobra.Command{
Use: "aro-hcp-frontend",
Version: version.CommitSHA,
Args: cobra.NoArgs,
Short: "Serve the ARO HCP Frontend",
Long: `Serve the ARO HCP Frontend
This command runs the ARO HCP Frontend. It communicates with Clusters Service and a CosmosDB
# Run ARO HCP Frontend locally to connect to a local Clusters Service at http://localhost:8000
./aro-hcp-frontend --cosmos-name ${DB_NAME} --cosmos-url ${DB_URL} --location ${LOCATION} \
--clusters-service-url "http://localhost:8000"
`,
RunE: func(cmd *cobra.Command, args []string) error {
return opts.Run()
},
}
rootCmd.Flags().StringVar(&opts.cosmosName, "cosmos-name", os.Getenv("DB_NAME"), "Cosmos database name")
rootCmd.Flags().StringVar(&opts.cosmosURL, "cosmos-url", os.Getenv("DB_URL"), "Cosmos database URL")
rootCmd.Flags().StringVar(&opts.location, "location", os.Getenv("LOCATION"), "Azure location")
rootCmd.Flags().IntVar(&opts.port, "port", 8443, "port to listen on")
rootCmd.Flags().IntVar(&opts.metricsPort, "metrics-port", 8081, "port to serve metrics on")
rootCmd.Flags().StringVar(&opts.clustersServiceURL, "clusters-service-url", "https://api.openshift.com", "URL of the OCM API gateway.")
rootCmd.Flags().BoolVar(&opts.insecure, "insecure", false, "Skip validating TLS for clusters-service.")
rootCmd.Flags().StringVar(&opts.clusterServiceProvisionShard, "cluster-service-provision-shard", "", "Manually specify provision shard for all requests to cluster service")
rootCmd.Flags().BoolVar(&opts.clusterServiceNoopProvision, "cluster-service-noop-provision", false, "Skip cluster service provisioning steps for development purposes")
rootCmd.Flags().BoolVar(&opts.clusterServiceNoopDeprovision, "cluster-service-noop-deprovision", false, "Skip cluster service deprovisioning steps for development purposes")
rootCmd.MarkFlagsRequiredTogether("cosmos-name", "cosmos-url")
return rootCmd
}
type policyFunc func(*policy.Request) (*http.Response, error)
func (pf policyFunc) Do(req *policy.Request) (*http.Response, error) {
return pf(req)
}
// Verify that policyFunc implements the policy.Policy interface.
var _ policy.Policy = policyFunc(nil)
// correlationIDPolicy adds the ARM correlation request ID to the request's
// HTTP headers if the ID is found in the context.
func correlationIDPolicy(req *policy.Request) (*http.Response, error) {
cd, err := frontend.CorrelationDataFromContext(req.Raw().Context())
// The incoming request may not contain a correlation request ID (e.g.
// requests to /healthz).
if err == nil && cd.CorrelationRequestID != "" {
req.Raw().Header.Set(arm.HeaderNameCorrelationRequestID, cd.CorrelationRequestID)
}
return req.Next()
}
func (opts *FrontendOpts) Run() error {
ctx := context.Background()
logger := util.DefaultLogger()
logger.Info(fmt.Sprintf("%s (%s) started", frontend.ProgramName, version.CommitSHA))
// Initialize the global OpenTelemetry tracer.
otelShutdown, err := frontend.ConfigureOpenTelemetryTracer(ctx, logger, semconv.CloudRegion(opts.location))
if err != nil {
return fmt.Errorf("could not initialize opentelemetry sdk: %w", err)
}
// Create the database client.
cosmosDatabaseClient, err := database.NewCosmosDatabaseClient(
opts.cosmosURL,
opts.cosmosName,
azcore.ClientOptions{
// FIXME Cloud should be determined by other means.
Cloud: cloud.AzurePublic,
PerCallPolicies: []policy.Policy{policyFunc(correlationIDPolicy)},
TracingProvider: azotel.NewTracingProvider(otel.GetTracerProvider(), nil),
},
)
if err != nil {
return fmt.Errorf("failed to create the CosmosDB client: %w", err)
}
dbClient, err := database.NewDBClient(ctx, cosmosDatabaseClient)
if err != nil {
return fmt.Errorf("failed to create the database client: %w", err)
}
listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", opts.port))
if err != nil {
return err
}
metricsListener, err := net.Listen("tcp4", fmt.Sprintf(":%d", opts.metricsPort))
if err != nil {
return err
}
// Initialize the Clusters Service Client.
conn, err := sdk.NewUnauthenticatedConnectionBuilder().
TransportWrapper(func(r http.RoundTripper) http.RoundTripper {
return otelhttp.NewTransport(
frontend.RequestIDPropagator(r),
)
}).
URL(opts.clustersServiceURL).
Insecure(opts.insecure).
MetricsSubsystem("frontend_clusters_service_client").
MetricsRegisterer(prometheus.DefaultRegisterer).
Build()
if err != nil {
return err
}
csClient := ocm.ClusterServiceClient{
Conn: conn,
ProvisionerNoOpProvision: opts.clusterServiceNoopDeprovision,
ProvisionerNoOpDeprovision: opts.clusterServiceNoopDeprovision,
}
if opts.clusterServiceProvisionShard != "" {
csClient.ProvisionShardID = api.Ptr(opts.clusterServiceProvisionShard)
}
if len(opts.location) == 0 {
return errors.New("location is required")
}
logger.Info(fmt.Sprintf("Application running in %s", opts.location))
f := frontend.NewFrontend(logger, listener, metricsListener, prometheus.DefaultRegisterer, dbClient, opts.location, &csClient)
stop := make(chan struct{})
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
go f.Run(ctx, stop)
sig := <-signalChannel
logger.Info(fmt.Sprintf("caught %s signal", sig))
close(stop)
f.Join()
_ = otelShutdown(ctx)
logger.Info(fmt.Sprintf("%s (%s) stopped", frontend.ProgramName, version.CommitSHA))
return nil
}