diff --git a/cmd/applications-rp/main.go b/cmd/applications-rp/main.go index 076930b086..57b93a00a0 100644 --- a/cmd/applications-rp/main.go +++ b/cmd/applications-rp/main.go @@ -18,21 +18,25 @@ package main import ( "context" - "flag" "fmt" "log" "os" "os/signal" "syscall" + "github.com/go-logr/logr" + "github.com/spf13/pflag" + etcdclient "go.etcd.io/etcd/client/v3" + runtimelog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/radius-project/radius/pkg/armrpc/builder" "github.com/radius-project/radius/pkg/armrpc/hostoptions" - "github.com/radius-project/radius/pkg/corerp/backend" - "github.com/radius-project/radius/pkg/corerp/frontend" metricsservice "github.com/radius-project/radius/pkg/metrics/service" profilerservice "github.com/radius-project/radius/pkg/profiler/service" + "github.com/radius-project/radius/pkg/recipes/controllerconfig" + "github.com/radius-project/radius/pkg/server" "github.com/radius-project/radius/pkg/trace" - "github.com/radius-project/radius/pkg/logging" pr_backend "github.com/radius-project/radius/pkg/portableresources/backend" pr_frontend "github.com/radius-project/radius/pkg/portableresources/frontend" "github.com/radius-project/radius/pkg/ucp/data" @@ -40,53 +44,31 @@ import ( "github.com/radius-project/radius/pkg/ucp/hosting" "github.com/radius-project/radius/pkg/ucp/ucplog" - "github.com/go-logr/logr" - etcdclient "go.etcd.io/etcd/client/v3" - runtimelog "sigs.k8s.io/controller-runtime/pkg/log" + corerp_setup "github.com/radius-project/radius/pkg/corerp/setup" ) -const serviceName = "applications.core" - -func newPortableResourceHosts(configFile string, enableAsyncWorker bool) ([]hosting.Service, *hostoptions.HostOptions, error) { - hostings := []hosting.Service{} - options, err := hostoptions.NewHostOptionsFromEnvironment(configFile) - if err != nil { - return nil, nil, err - } - hostings = append(hostings, pr_frontend.NewService(options)) - if enableAsyncWorker { - hostings = append(hostings, pr_backend.NewService(options)) - } - - return hostings, &options, nil -} +const serviceName = "radius" func main() { var configFile string - var enableAsyncWorker bool - - var runPortableResource bool - var portableResourceConfigFile string - defaultConfig := fmt.Sprintf("radius-%s.yaml", hostoptions.Environment()) - flag.StringVar(&configFile, "config-file", defaultConfig, "The service configuration file.") - flag.BoolVar(&enableAsyncWorker, "enable-asyncworker", true, "Flag to run async request process worker (for private preview and dev/test purpose).") - - flag.BoolVar(&runPortableResource, "run-portableresource", true, "Flag to run portable resources RPs(for private preview and dev/test purpose).") - defaultPortableRsConfig := fmt.Sprintf("portableresource-%s.yaml", hostoptions.Environment()) - flag.StringVar(&portableResourceConfigFile, "portableresource-config", defaultPortableRsConfig, "The service configuration file for portable resource providers.") - + pflag.StringVar(&configFile, "config-file", defaultConfig, "The service configuration file.") if configFile == "" { log.Fatal("config-file is empty.") //nolint:forbidigo // this is OK inside the main function. } - flag.Parse() + var portableResourceConfigFile string + defaultPortableRsConfig := fmt.Sprintf("portableresource-%s.yaml", hostoptions.Environment()) + pflag.StringVar(&portableResourceConfigFile, "portableresource-config", defaultPortableRsConfig, "The service configuration file for portable resource providers.") + + pflag.Parse() options, err := hostoptions.NewHostOptionsFromEnvironment(configFile) if err != nil { log.Fatal(err) //nolint:forbidigo // this is OK inside the main function. } - hostingSvc := []hosting.Service{frontend.NewService(options)} + + hostingSvc := []hosting.Service{} metricOptions := metricsservice.NewHostOptionsFromEnvironment(*options.Config) metricOptions.Config.ServiceName = serviceName @@ -99,7 +81,7 @@ func main() { hostingSvc = append(hostingSvc, profilerservice.NewService(profilerOptions)) } - logger, flush, err := ucplog.NewLogger(logging.AppCoreLoggerName, &options.Config.Logging) + logger, flush, err := ucplog.NewLogger(serviceName, &options.Config.Logging) if err != nil { log.Fatal(err) //nolint:forbidigo // this is OK inside the main function. } @@ -108,22 +90,10 @@ func main() { // Must set the logger before using controller-runtime. runtimelog.SetLogger(logger) - if enableAsyncWorker { - logger.Info("Enable AsyncRequestProcessWorker.") - hostingSvc = append(hostingSvc, backend.NewService(options)) - } - - // Configure Portable Resources to run it with Applications.Core RP. - var portableResourceOpts *hostoptions.HostOptions - if runPortableResource && portableResourceConfigFile != "" { - logger.Info("Run Service for Portable Resource Providers.") - var portableResourceSvcs []hosting.Service - var err error - portableResourceSvcs, portableResourceOpts, err = newPortableResourceHosts(portableResourceConfigFile, enableAsyncWorker) - if err != nil { - log.Fatal(err) //nolint:forbidigo // this is OK inside the main function. - } - hostingSvc = append(hostingSvc, portableResourceSvcs...) + // Load portable resource config. + prOptions, err := hostoptions.NewHostOptionsFromEnvironment(portableResourceConfigFile) + if err != nil { + log.Fatal(err) //nolint:forbidigo // this is OK inside the main function. } if options.Config.StorageProvider.Provider == dataprovider.TypeETCD && @@ -135,13 +105,31 @@ func main() { client := hosting.NewAsyncValue[etcdclient.Client]() options.Config.StorageProvider.ETCD.Client = client options.Config.SecretProvider.ETCD.Client = client - if portableResourceOpts != nil { - portableResourceOpts.Config.StorageProvider.ETCD.Client = client - portableResourceOpts.Config.SecretProvider.ETCD.Client = client - } + + // Portable resource options + prOptions.Config.StorageProvider.ETCD.Client = client + prOptions.Config.SecretProvider.ETCD.Client = client + hostingSvc = append(hostingSvc, data.NewEmbeddedETCDService(data.EmbeddedETCDServiceOptions{ClientConfigSink: client})) } + builders, err := builders(options) + if err != nil { + log.Fatal(err) //nolint:forbidigo // this is OK inside the main function. + } + + hostingSvc = append( + hostingSvc, + server.NewAPIService(options, builders), + server.NewAsyncWorker(options, builders), + + // Configure Portable Resources to run it with Applications.Core RP. + // + // This is temporary until we migrate these resources to use the new registration model. + pr_frontend.NewService(prOptions), + pr_backend.NewService(prOptions), + ) + loggerValues := []any{} host := &hosting.Host{ Services: hostingSvc, @@ -190,3 +178,15 @@ func main() { panic(err) } } + +func builders(options hostoptions.HostOptions) ([]builder.Builder, error) { + config, err := controllerconfig.New(options) + if err != nil { + return nil, err + } + + return []builder.Builder{ + corerp_setup.SetupNamespace(config).GenerateBuilder(), + // Add resource provider builders... + }, nil +} diff --git a/cmd/applications-rp/portableresource-dev.yaml b/cmd/applications-rp/portableresource-dev.yaml index 77c5a933b0..89fd579140 100644 --- a/cmd/applications-rp/portableresource-dev.yaml +++ b/cmd/applications-rp/portableresource-dev.yaml @@ -8,6 +8,7 @@ storageProvider: inmemory: true queueProvider: provider: inmemory + name: radiusportable profilerProvider: enabled: true port: 6060 diff --git a/cmd/applications-rp/portableresource-self-hosted.yaml b/cmd/applications-rp/portableresource-self-hosted.yaml index fdadd10dda..37c587e086 100644 --- a/cmd/applications-rp/portableresource-self-hosted.yaml +++ b/cmd/applications-rp/portableresource-self-hosted.yaml @@ -17,6 +17,7 @@ storageProvider: namespace: 'radius-testing' queueProvider: provider: "apiserver" + name: radiusportable apiserver: context: '' namespace: 'radius-testing' diff --git a/cmd/applications-rp/radius-cloud.yaml b/cmd/applications-rp/radius-cloud.yaml index 8555e931fa..66c36258a2 100644 --- a/cmd/applications-rp/radius-cloud.yaml +++ b/cmd/applications-rp/radius-cloud.yaml @@ -19,6 +19,7 @@ storageProvider: masterKey: set-me-in-a-different-way queueProvider: provider: inmemory + name: radius profilerProvider: enabled: true port: 6060 diff --git a/cmd/applications-rp/radius-dev.yaml b/cmd/applications-rp/radius-dev.yaml index a679384d92..3c64758fe6 100644 --- a/cmd/applications-rp/radius-dev.yaml +++ b/cmd/applications-rp/radius-dev.yaml @@ -8,6 +8,7 @@ storageProvider: inmemory: true queueProvider: provider: inmemory + name: radius profilerProvider: enabled: true port: 6060 diff --git a/cmd/applications-rp/radius-self-hosted.yaml b/cmd/applications-rp/radius-self-hosted.yaml index 63090e1448..b247043a15 100644 --- a/cmd/applications-rp/radius-self-hosted.yaml +++ b/cmd/applications-rp/radius-self-hosted.yaml @@ -17,6 +17,7 @@ storageProvider: namespace: 'radius-testing' queueProvider: provider: "apiserver" + name: radius apiserver: context: '' namespace: 'radius-testing' diff --git a/cmd/ucpd/ucp-self-hosted-dev.yaml b/cmd/ucpd/ucp-self-hosted-dev.yaml index 5f14d8a49b..8e73d30861 100644 --- a/cmd/ucpd/ucp-self-hosted-dev.yaml +++ b/cmd/ucpd/ucp-self-hosted-dev.yaml @@ -20,6 +20,7 @@ secretProvider: queueProvider: provider: "apiserver" + name: 'ucp' apiserver: context: '' namespace: 'radius-testing' diff --git a/deploy/Chart/templates/rp/configmaps.yaml b/deploy/Chart/templates/rp/configmaps.yaml index a66ed7437b..8dbecbc90e 100644 --- a/deploy/Chart/templates/rp/configmaps.yaml +++ b/deploy/Chart/templates/rp/configmaps.yaml @@ -20,6 +20,7 @@ data: namespace: "radius-system" queueProvider: provider: "apiserver" + name: "radius" apiserver: context: "" namespace: "radius-system" @@ -66,6 +67,7 @@ data: namespace: "radius-system" queueProvider: provider: "apiserver" + name: "radiusportable" apiserver: context: "" namespace: "radius-system" diff --git a/deploy/Chart/templates/rp/deployment.yaml b/deploy/Chart/templates/rp/deployment.yaml index 3da0d8dbe4..d3ee662bd9 100644 --- a/deploy/Chart/templates/rp/deployment.yaml +++ b/deploy/Chart/templates/rp/deployment.yaml @@ -32,7 +32,6 @@ spec: image: "{{ .Values.rp.image }}:{{ .Values.rp.tag | default $appversion }}" args: - --config-file=/etc/config/radius-self-host.yaml - - --run-portableresource - --portableresource-config=/etc/config/portableresource-self-host.yaml env: - name: SKIP_ARM diff --git a/deploy/Chart/templates/ucp/configmaps.yaml b/deploy/Chart/templates/ucp/configmaps.yaml index 7d9a8154cd..95b27be087 100644 --- a/deploy/Chart/templates/ucp/configmaps.yaml +++ b/deploy/Chart/templates/ucp/configmaps.yaml @@ -20,7 +20,11 @@ data: provider: kubernetes queueProvider: - provider: inmemory + provider: "apiserver" + name: "ucp" + apiserver: + context: "" + namespace: "radius-system" profilerProvider: enabled: true diff --git a/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md b/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md index 3d8723dafe..2043051d22 100644 --- a/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md +++ b/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md @@ -197,6 +197,7 @@ storageProvider: namespace: "radius-system" queueProvider: provider: "apiserver" + name: "radius" apiserver: context: "" namespace: "radius-system" diff --git a/pkg/armrpc/api/v1/types.go b/pkg/armrpc/api/v1/types.go index 5e4ed75020..623a2fafbc 100644 --- a/pkg/armrpc/api/v1/types.go +++ b/pkg/armrpc/api/v1/types.go @@ -44,17 +44,15 @@ var ( type OperationMethod string var operationMethodToHTTPMethod = map[OperationMethod]string{ - OperationList: http.MethodGet, - OperationGet: http.MethodGet, - OperationPut: http.MethodPut, - OperationPatch: http.MethodPatch, - OperationDelete: http.MethodDelete, + OperationPlaneScopeList: http.MethodGet, + OperationList: http.MethodGet, + OperationGet: http.MethodGet, + OperationPut: http.MethodPut, + OperationPatch: http.MethodPatch, + OperationDelete: http.MethodDelete, // ARM RPC specific operations. - OperationGetOperations: http.MethodGet, - OperationGetOperationStatuses: http.MethodGet, - OperationGetOperationResult: http.MethodGet, - OperationPutSubscriptions: http.MethodPut, + OperationPutSubscriptions: http.MethodPut, // Non-idempotent lifecycle operations. OperationGetImperative: http.MethodPost, @@ -80,16 +78,14 @@ func (o OperationMethod) HTTPMethod() string { const ( // Predefined Operation methods. - OperationList OperationMethod = "LIST" - OperationGet OperationMethod = "GET" - OperationPut OperationMethod = "PUT" - OperationPatch OperationMethod = "PATCH" - OperationDelete OperationMethod = "DELETE" - OperationGetOperations OperationMethod = "GETOPERATIONS" - OperationGetOperationStatuses OperationMethod = "GETOPERATIONSTATUSES" - OperationGetOperationResult OperationMethod = "GETOPERATIONRESULT" - OperationPutSubscriptions OperationMethod = "PUTSUBSCRIPTIONS" - OperationPost OperationMethod = "POST" + OperationPlaneScopeList OperationMethod = "LISTPLANESCOPE" + OperationList OperationMethod = "LIST" + OperationGet OperationMethod = "GET" + OperationPut OperationMethod = "PUT" + OperationPatch OperationMethod = "PATCH" + OperationDelete OperationMethod = "DELETE" + OperationPutSubscriptions OperationMethod = "PUTSUBSCRIPTIONS" + OperationPost OperationMethod = "POST" // Imperative operation methods for non-idempotent lifecycle operations. // UCP extends the ARM resource lifecycle to support using POST for non-idempotent resource types. diff --git a/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go b/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go index 954324b755..a51525c2ca 100644 --- a/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go +++ b/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go @@ -27,6 +27,7 @@ import ( ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" "github.com/radius-project/radius/pkg/metrics" "github.com/radius-project/radius/pkg/trace" + "github.com/radius-project/radius/pkg/ucp/dataprovider" queue "github.com/radius-project/radius/pkg/ucp/queue/client" "github.com/radius-project/radius/pkg/ucp/resources" "github.com/radius-project/radius/pkg/ucp/store" @@ -36,10 +37,9 @@ import ( // statusManager includes the necessary functions to manage asynchronous operations. type statusManager struct { - storeClient store.StorageClient - queue queue.Client - providerName string - location string + storeProvider dataprovider.DataStorageProvider + queue queue.Client + location string } // QueueOperationOptions is the options type provided when queueing an async operation. @@ -65,12 +65,11 @@ type StatusManager interface { } // New creates statusManager instance. -func New(storeClient store.StorageClient, q queue.Client, providerName, location string) StatusManager { +func New(dataProvider dataprovider.DataStorageProvider, q queue.Client, location string) StatusManager { return &statusManager{ - storeClient: storeClient, - queue: q, - providerName: providerName, - location: location, + storeProvider: dataProvider, + queue: q, + location: location, } } @@ -79,6 +78,10 @@ func (aom *statusManager) operationStatusResourceID(id resources.ID, operationID return fmt.Sprintf("%s/providers/%s/locations/%s/operationstatuses/%s", id.PlaneScope(), strings.ToLower(id.ProviderNamespace()), aom.location, operationID) } +func (aom *statusManager) getClient(ctx context.Context, id resources.ID) (store.StorageClient, error) { + return aom.storeProvider.GetStorageClient(ctx, id.ProviderNamespace()+"/operationstatuses") +} + // QueueAsyncOperation creates and saves a new status resource with the given parameters in datastore, and queues // a request message. If an error occurs, the status is deleted using the storeClient. func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMRequestContext, options QueueOperationOptions) error { @@ -108,7 +111,12 @@ func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMR ClientObjectID: sCtx.ClientObjectID, } - err := aom.storeClient.Save(ctx, &store.Object{ + storeClient, err := aom.getClient(ctx, sCtx.ResourceID) + if err != nil { + return err + } + + err = storeClient.Save(ctx, &store.Object{ Metadata: store.Metadata{ID: opID}, Data: aos, }) @@ -118,7 +126,7 @@ func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMR } if err = aom.queueRequestMessage(ctx, sCtx, aos, options.OperationTimeout); err != nil { - delErr := aom.storeClient.Delete(ctx, opID) + delErr := storeClient.Delete(ctx, opID) if delErr != nil { return delErr } @@ -132,7 +140,12 @@ func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMR // Get gets a status object from the datastore or an error if the retrieval fails. func (aom *statusManager) Get(ctx context.Context, id resources.ID, operationID uuid.UUID) (*Status, error) { - obj, err := aom.storeClient.Get(ctx, aom.operationStatusResourceID(id, operationID)) + storeClient, err := aom.getClient(ctx, id) + if err != nil { + return nil, err + } + + obj, err := storeClient.Get(ctx, aom.operationStatusResourceID(id, operationID)) if err != nil { return nil, err } @@ -149,8 +162,12 @@ func (aom *statusManager) Get(ctx context.Context, id resources.ID, operationID // given parameters, and saves it back to the store. func (aom *statusManager) Update(ctx context.Context, id resources.ID, operationID uuid.UUID, state v1.ProvisioningState, endTime *time.Time, opError *v1.ErrorDetails) error { opID := aom.operationStatusResourceID(id, operationID) + storeClient, err := aom.getClient(ctx, id) + if err != nil { + return err + } - obj, err := aom.storeClient.Get(ctx, opID) + obj, err := storeClient.Get(ctx, opID) if err != nil { return err } @@ -173,13 +190,17 @@ func (aom *statusManager) Update(ctx context.Context, id resources.ID, operation obj.Data = s - return aom.storeClient.Save(ctx, obj, store.WithETag(obj.ETag)) + return storeClient.Save(ctx, obj, store.WithETag(obj.ETag)) } // Delete deletes the operation status resource associated with the given ID and // operationID, and returns an error if unsuccessful. func (aom *statusManager) Delete(ctx context.Context, id resources.ID, operationID uuid.UUID) error { - return aom.storeClient.Delete(ctx, aom.operationStatusResourceID(id, operationID)) + storeClient, err := aom.getClient(ctx, id) + if err != nil { + return err + } + return storeClient.Delete(ctx, aom.operationStatusResourceID(id, operationID)) } // queueRequestMessage function is to put the async operation message to the queue to be worked on. diff --git a/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go b/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go index 8a3fa6b87a..43b88d0482 100644 --- a/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go +++ b/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go @@ -26,6 +26,7 @@ import ( "github.com/google/uuid" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/radius-project/radius/pkg/ucp/dataprovider" queue "github.com/radius-project/radius/pkg/ucp/queue/client" "github.com/radius-project/radius/pkg/ucp/resources" "github.com/radius-project/radius/pkg/ucp/store" @@ -33,9 +34,10 @@ import ( ) type asyncOperationsManagerTest struct { - manager StatusManager - storeClient *store.MockStorageClient - queue *queue.MockClient + manager StatusManager + storeProvider *dataprovider.MockDataStorageProvider + storeClient *store.MockStorageClient + queue *queue.MockClient } const ( @@ -51,13 +53,16 @@ const ( func setup(tb testing.TB) (asyncOperationsManagerTest, *gomock.Controller) { ctrl := gomock.NewController(tb) + dp := dataprovider.NewMockDataStorageProvider(ctrl) sc := store.NewMockStorageClient(ctrl) + dp.EXPECT().GetStorageClient(gomock.Any(), "Applications.Core/operationstatuses").Return(sc, nil) enq := queue.NewMockClient(ctrl) - aom := New(sc, enq, "Test-AsyncOperationsManager", "test-location") - return asyncOperationsManagerTest{manager: aom, storeClient: sc, queue: enq}, ctrl + aom := New(dp, enq, "test-location") + return asyncOperationsManagerTest{manager: aom, storeProvider: dp, storeClient: sc, queue: enq}, ctrl } var reqCtx = &v1.ARMRequestContext{ + ResourceID: resources.MustParse("/planes/radius/local/resourceGroups/radius-test-rg/providers/Applications.Core/container/container0"), OperationID: uuid.Must(uuid.NewRandom()), HomeTenantID: "home-tenant-id", ClientObjectID: "client-object-id", @@ -99,7 +104,7 @@ func TestOperationStatusResourceID(t *testing.T) { }, } - sm := &statusManager{providerName: "applications.core", location: v1.LocationGlobal} + sm := &statusManager{location: v1.LocationGlobal} for _, tc := range resourceIDTests { t.Run(tc.resourceID, func(t *testing.T) { diff --git a/pkg/armrpc/asyncoperation/worker/registry.go b/pkg/armrpc/asyncoperation/worker/registry.go index 4a178e7b7f..814fc85b6d 100644 --- a/pkg/armrpc/asyncoperation/worker/registry.go +++ b/pkg/armrpc/asyncoperation/worker/registry.go @@ -49,7 +49,7 @@ func (h *ControllerRegistry) Register(ctx context.Context, resourceType string, ot := v1.OperationType{Type: resourceType, Method: method} - storageClient, err := opts.DataProvider.GetStorageClient(ctx, resourceType) + storageClient, err := h.sp.GetStorageClient(ctx, resourceType) if err != nil { return err } diff --git a/pkg/armrpc/asyncoperation/worker/service.go b/pkg/armrpc/asyncoperation/worker/service.go index d2ce0cec69..4a04a19384 100644 --- a/pkg/armrpc/asyncoperation/worker/service.go +++ b/pkg/armrpc/asyncoperation/worker/service.go @@ -21,18 +21,10 @@ import ( manager "github.com/radius-project/radius/pkg/armrpc/asyncoperation/statusmanager" "github.com/radius-project/radius/pkg/armrpc/hostoptions" - "github.com/radius-project/radius/pkg/kubeutil" "github.com/radius-project/radius/pkg/ucp/dataprovider" queue "github.com/radius-project/radius/pkg/ucp/queue/client" qprovider "github.com/radius-project/radius/pkg/ucp/queue/provider" "github.com/radius-project/radius/pkg/ucp/ucplog" - - "k8s.io/client-go/discovery" - "k8s.io/client-go/kubernetes" - - "k8s.io/client-go/dynamic" - - controller_runtime "sigs.k8s.io/controller-runtime/pkg/client" ) // Service is the base worker service implementation to initialize and start worker. @@ -49,58 +41,20 @@ type Service struct { Controllers *ControllerRegistry // RequestQueue is the queue client for async operation request message. RequestQueue queue.Client - // KubeClient is the Kubernetes controller runtime client. - KubeClient controller_runtime.Client - // KubeClientSet is the Kubernetes client. - KubeClientSet kubernetes.Interface - // KubeDiscoveryClient is the Kubernetes discovery client. - KubeDiscoveryClient discovery.ServerResourcesInterface - // KubeDynamicClientSet is the Kubernetes dynamic client. - KubeDynamicClientSet dynamic.Interface } // Init initializes worker service - it initializes the StorageProvider, RequestQueue, OperationStatusManager, Controllers, KubeClient and // returns an error if any of these operations fail. func (s *Service) Init(ctx context.Context) error { s.StorageProvider = dataprovider.NewStorageProvider(s.Options.Config.StorageProvider) - qp := qprovider.New(s.ProviderName, s.Options.Config.QueueProvider) - opSC, err := s.StorageProvider.GetStorageClient(ctx, s.ProviderName+"/operationstatuses") - if err != nil { - return err - } + qp := qprovider.New(s.Options.Config.QueueProvider) + var err error s.RequestQueue, err = qp.GetClient(ctx) if err != nil { return err } - s.OperationStatusManager = manager.New(opSC, s.RequestQueue, s.ProviderName, s.Options.Config.Env.RoleLocation) + s.OperationStatusManager = manager.New(s.StorageProvider, s.RequestQueue, s.Options.Config.Env.RoleLocation) s.Controllers = NewControllerRegistry(s.StorageProvider) - - if s.Options.K8sConfig != nil { - s.KubeClient, err = kubeutil.NewRuntimeClient(s.Options.K8sConfig) - if err != nil { - return err - } - - s.KubeClientSet, err = kubernetes.NewForConfig(s.Options.K8sConfig) - if err != nil { - return err - } - - discoveryClient, err := discovery.NewDiscoveryClientForConfig(s.Options.K8sConfig) - if err != nil { - return err - } - - // Use legacy discovery client to avoid the issue of the staled GroupVersion discovery(api.ucp.dev/v1alpha3). - // TODO: Disable UseLegacyDiscovery once https://github.com/radius-project/radius/issues/5974 is resolved. - discoveryClient.UseLegacyDiscovery = true - s.KubeDiscoveryClient = discoveryClient - - s.KubeDynamicClientSet, err = dynamic.NewForConfig(s.Options.K8sConfig) - if err != nil { - return err - } - } return nil } diff --git a/pkg/armrpc/builder/builder.go b/pkg/armrpc/builder/builder.go new file mode 100644 index 0000000000..3284b7968a --- /dev/null +++ b/pkg/armrpc/builder/builder.go @@ -0,0 +1,184 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker" + apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation" + "github.com/radius-project/radius/pkg/armrpc/frontend/server" + "github.com/radius-project/radius/pkg/validator" + "github.com/radius-project/radius/swagger" +) + +// Builder can be used to register operations and build HTTP routing paths and handlers for a resource namespace. +type Builder struct { + namespaceNode *Namespace + registrations []*OperationRegistration +} + +// defaultHandlerOptions returns HandlerOption for the default operations such as getting operationStatuses and +// operationResults. +func defaultHandlerOptions( + ctx context.Context, + rootRouter chi.Router, + rootScopePath string, + namespace string, + availableOperations []v1.Operation, + ctrlOpts apictrl.Options) []server.HandlerOptions { + namespace = strings.ToLower(namespace) + + handlers := []server.HandlerOptions{} + if len(availableOperations) > 0 { + // https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/proxy-api-reference.md#exposing-available-operations + handlers = append(handlers, server.HandlerOptions{ + ParentRouter: rootRouter, + Path: rootScopePath + "/providers/" + namespace + "/operations", + ResourceType: namespace + "/operations", + Method: v1.OperationGet, + ControllerFactory: func(op apictrl.Options) (apictrl.Controller, error) { + return defaultoperation.NewGetOperations(op, availableOperations) + }, + }) + } + + statusType := namespace + "/operationstatuses" + resultType := namespace + "/operationresults" + handlers = append(handlers, server.HandlerOptions{ + ParentRouter: rootRouter, + Path: fmt.Sprintf("%s/providers/%s/locations/{location}/operationstatuses/{operationId}", rootScopePath, namespace), + ResourceType: statusType, + Method: v1.OperationGet, + ControllerFactory: defaultoperation.NewGetOperationStatus, + }) + + handlers = append(handlers, server.HandlerOptions{ + ParentRouter: rootRouter, + Path: fmt.Sprintf("%s/providers/%s/locations/{location}/operationresults/{operationId}", rootScopePath, namespace), + ResourceType: resultType, + Method: v1.OperationGet, + ControllerFactory: defaultoperation.NewGetOperationResult, + }) + + return handlers +} + +func (b *Builder) Namespace() string { + return b.namespaceNode.Name +} + +const ( + UCPRootScopePath = "/planes/radius/{planeName}" + ResourceGroupPath = "/resourcegroups/{resourceGroupName}" +) + +// NewOpenAPIValidatorMiddleware creates a new OpenAPI validator middleware. +func NewOpenAPIValidator(ctx context.Context, base, namespace string) (func(h http.Handler) http.Handler, error) { + rootScopePath := base + UCPRootScopePath + + // URLs may use either the subscription/plane scope or resource group scope. + // These paths are order sensitive and the longer path MUST be registered first. + prefixes := []string{ + rootScopePath + ResourceGroupPath, + rootScopePath, + } + + specLoader, err := validator.LoadSpec(ctx, namespace, swagger.SpecFiles, prefixes, "rootScope") + if err != nil { + return nil, err + } + + return validator.APIValidator(validator.Options{ + SpecLoader: specLoader, + ResourceTypeGetter: validator.RadiusResourceTypeGetter, + }), nil +} + +// ApplyAPIHandlers builds HTTP routing paths and handlers for namespace. +func (b *Builder) ApplyAPIHandlers(ctx context.Context, r chi.Router, ctrlOpts apictrl.Options, middlewares ...func(h http.Handler) http.Handler) error { + rootScopePath := ctrlOpts.PathBase + UCPRootScopePath + + // Configure the default handlers. + handlerOptions := defaultHandlerOptions(ctx, r, rootScopePath, b.namespaceNode.Name, b.namespaceNode.availableOperations, ctrlOpts) + + routerMap := map[string]chi.Router{} + for _, h := range b.registrations { + if h == nil { + continue + } + + key := "" + route := "" + switch h.Method { + case v1.OperationPlaneScopeList: + route = fmt.Sprintf("%s/providers/%s", rootScopePath, strings.ToLower(h.ResourceType)) + key = "plane-" + h.ResourceType + case v1.OperationList: + route = fmt.Sprintf("%s/resourcegroups/{resourceGroupName}/providers/%s", rootScopePath, h.ResourceNamePattern) + key = "rg-" + h.ResourceType + default: + route = fmt.Sprintf("%s/resourcegroups/{resourceGroupName}/providers/%s", rootScopePath, h.ResourceNamePattern) + key = "resource-" + h.ResourceNamePattern + } + + if _, ok := routerMap[key]; !ok { + routerMap[key] = server.NewSubrouter(r, route, middlewares...) + } + + handlerOptions = append(handlerOptions, server.HandlerOptions{ + ParentRouter: routerMap[key], + Path: strings.ToLower(h.Path), + ResourceType: h.ResourceType, + Method: h.Method, + ControllerFactory: h.APIController, + }) + } + + for _, o := range handlerOptions { + if err := server.RegisterHandler(ctx, o, ctrlOpts); err != nil { + return err + } + } + + return nil +} + +// ApplyAsyncHandler registers asynchronous controllers from HandlerOutput. +func (b *Builder) ApplyAsyncHandler(ctx context.Context, registry *worker.ControllerRegistry, ctrlOpts asyncctrl.Options) error { + for _, h := range b.registrations { + if h == nil { + continue + } + + if h.AsyncController != nil { + err := registry.Register(ctx, h.ResourceType, h.Method, h.AsyncController, ctrlOpts) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/armrpc/builder/builder_test.go b/pkg/armrpc/builder/builder_test.go new file mode 100644 index 0000000000..480aa69765 --- /dev/null +++ b/pkg/armrpc/builder/builder_test.go @@ -0,0 +1,267 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "context" + "net/http" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/golang/mock/gomock" + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker" + apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/radius-project/radius/pkg/ucp/dataprovider" + "github.com/radius-project/radius/pkg/ucp/store" + "github.com/radius-project/radius/test/testcontext" + "github.com/stretchr/testify/require" +) + +var handlerTests = []rpctest.HandlerTestSpec{ + // applications.compute/virtualMachines + { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.compute/virtualmachines", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: "ACTIONSTART"}, + Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0/start", + Method: http.MethodPost, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: "ACTIONSTOP"}, + Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0/stop", + Method: http.MethodPost, + }, + // applications.compute/containers + { + OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.compute/containers", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: "ACTIONGETRESOURCE"}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/getresource", + Method: http.MethodPost, + }, + // applications.compute/containers/secrets + { + OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0", + Method: http.MethodDelete, + }, + // applications.compute/webassemblies + { + OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.compute/webassemblies", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0", + Method: http.MethodDelete, + }, +} + +var defaultHandlerTests = []rpctest.HandlerTestSpec{ + { + OperationType: v1.OperationType{Type: "Applications.Compute/operations", Method: v1.OperationGet}, + Path: "/providers/applications.compute/operations", + Method: http.MethodGet, + }, + // default operations + { + OperationType: v1.OperationType{Type: "Applications.Compute/operationStatuses", Method: v1.OperationGet}, + Path: "/providers/applications.compute/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Compute/operationResults", Method: v1.OperationGet}, + Path: "/providers/applications.compute/locations/global/operationresults/00000000-0000-0000-0000-000000000000", + Method: http.MethodGet, + }, +} + +func setup(t *testing.T) (*dataprovider.MockDataStorageProvider, *store.MockStorageClient) { + mctrl := gomock.NewController(t) + + mockSP := dataprovider.NewMockDataStorageProvider(mctrl) + mockSC := store.NewMockStorageClient(mctrl) + + mockSC.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&store.Object{}, nil).AnyTimes() + mockSC.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + mockSC.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + mockSC.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(&store.ObjectQueryResult{}, nil).AnyTimes() + mockSP.EXPECT().GetStorageClient(gomock.Any(), gomock.Any()).Return(store.StorageClient(mockSC), nil).AnyTimes() + + return mockSP, mockSC +} + +func TestApplyAPIHandlers(t *testing.T) { + mockSP, _ := setup(t) + + runTests := func(t *testing.T, testSpecs []rpctest.HandlerTestSpec, b *Builder) { + rpctest.AssertRequests(t, testSpecs, "/api.ucp.dev", "/planes/radius/local", func(ctx context.Context) (chi.Router, error) { + r := chi.NewRouter() + return r, b.ApplyAPIHandlers(ctx, r, apictrl.Options{PathBase: "/api.ucp.dev", DataProvider: mockSP}) + }) + } + + t.Run("custom handlers", func(t *testing.T) { + ns := newTestNamespace(t) + builder := ns.GenerateBuilder() + runTests(t, handlerTests, &builder) + }) + + t.Run("default handlers", func(t *testing.T) { + ns := newTestNamespace(t) + builder := ns.GenerateBuilder() + ns.SetAvailableOperations([]v1.Operation{ + { + Name: "Applications.Compute/operations/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Compute", + Resource: "operations", + Operation: "Get operations", + Description: "Get the list of operations", + }, + IsDataAction: false, + }, + }) + runTests(t, defaultHandlerTests, &builder) + }) +} + +func TestApplyAPIHandlers_AvailableOperations(t *testing.T) { + mockSP, _ := setup(t) + ns := newTestNamespace(t) + + ns.SetAvailableOperations([]v1.Operation{ + { + Name: "Applications.Compute/operations/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Compute", + Resource: "operations", + Operation: "Get operations", + Description: "Get the list of operations", + }, + IsDataAction: false, + }, + }) + + builder := ns.GenerateBuilder() + rpctest.AssertRequests(t, handlerTests, "/api.ucp.dev", "/planes/radius/local", func(ctx context.Context) (chi.Router, error) { + r := chi.NewRouter() + return r, builder.ApplyAPIHandlers(ctx, r, apictrl.Options{PathBase: "/api.ucp.dev", DataProvider: mockSP}) + }) +} + +func TestApplyAsyncHandler(t *testing.T) { + mockSP, _ := setup(t) + ns := newTestNamespace(t) + builder := ns.GenerateBuilder() + registry := worker.NewControllerRegistry(mockSP) + ctx := testcontext.New(t) + err := builder.ApplyAsyncHandler(ctx, registry, asyncctrl.Options{}) + require.NoError(t, err) + + expectedOperations := []v1.OperationType{ + {Type: "Applications.Compute/virtualMachines", Method: v1.OperationPut}, + {Type: "Applications.Compute/virtualMachines", Method: v1.OperationPatch}, + {Type: "Applications.Compute/virtualMachines", Method: "ACTIONSTART"}, + {Type: "Applications.Compute/virtualMachines/disks", Method: v1.OperationPut}, + {Type: "Applications.Compute/virtualMachines/disks", Method: v1.OperationPatch}, + {Type: "Applications.Compute/webAssemblies", Method: v1.OperationPut}, + {Type: "Applications.Compute/webAssemblies", Method: v1.OperationPatch}, + } + + for _, op := range expectedOperations { + jobCtrl := registry.Get(op) + require.NotNil(t, jobCtrl) + } +} diff --git a/pkg/armrpc/builder/namespace.go b/pkg/armrpc/builder/namespace.go new file mode 100644 index 0000000000..e0791e04a0 --- /dev/null +++ b/pkg/armrpc/builder/namespace.go @@ -0,0 +1,84 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "strings" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" +) + +// Namespace represents the namespace of UCP. +type Namespace struct { + ResourceNode + + // availableOperations is the list of available operations for the namespace. + availableOperations []v1.Operation +} + +// NewNamespace creates a new namespace. +func NewNamespace(namespace string) *Namespace { + return &Namespace{ + ResourceNode: ResourceNode{ + Kind: NamespaceResourceKind, + Name: namespace, + children: make(map[string]*ResourceNode), + }, + } +} + +// SetAvailableOperations sets the available operations for the namespace. +func (p *Namespace) SetAvailableOperations(operations []v1.Operation) { + p.availableOperations = operations +} + +// GenerateBuilder Builder object by traversing resource nodes from namespace. +func (p *Namespace) GenerateBuilder() Builder { + return Builder{ + namespaceNode: p, + registrations: p.resolve(&p.ResourceNode, p.Name, strings.ToLower(p.Name)), + } +} + +func (p *Namespace) resolve(node *ResourceNode, qualifiedType string, qualifiedPattern string) []*OperationRegistration { + outputs := []*OperationRegistration{} + + newType := qualifiedType + newPattern := qualifiedPattern + + if node.Kind != NamespaceResourceKind { + newType = qualifiedType + "/" + node.Name + newPattern = qualifiedPattern + "/" + strings.ToLower(node.Name) + newParamName := "{" + node.option.ParamName() + "}" + + // This builds the handler outputs for each resource type. + ctrls := node.option.BuildHandlerOutputs(BuildOptions{ + ResourceType: newType, + ParameterName: newParamName, + ResourceNamePattern: newPattern, + }) + + newPattern += "/" + newParamName + outputs = append(outputs, ctrls...) + } + + for _, child := range node.children { + outputs = append(outputs, p.resolve(child, newType, newPattern)...) + } + + return outputs +} diff --git a/pkg/armrpc/builder/namespace_test.go b/pkg/armrpc/builder/namespace_test.go new file mode 100644 index 0000000000..b247c72810 --- /dev/null +++ b/pkg/armrpc/builder/namespace_test.go @@ -0,0 +1,445 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "context" + "net/http" + "testing" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/rest" + "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/radius-project/radius/pkg/ucp/store" + "github.com/stretchr/testify/require" +) + +type testAPIController struct { + apictrl.Operation[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel] +} + +func (e *testAPIController) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) { + return nil, nil +} + +type testAsyncController struct { +} + +func (c *testAsyncController) Run(ctx context.Context, request *asyncctrl.Request) (asyncctrl.Result, error) { + return asyncctrl.Result{}, nil +} + +func (c *testAsyncController) StorageClient() store.StorageClient { + return nil +} + +func newTestController(opts apictrl.Options) (apictrl.Controller, error) { + return &testAPIController{ + apictrl.NewOperation(opts, + apictrl.ResourceOptions[rpctest.TestResourceDataModel]{ + RequestConverter: rpctest.TestResourceDataModelFromVersioned, + ResponseConverter: rpctest.TestResourceDataModelToVersioned, + }, + ), + }, nil +} + +func newTestNamespace(t *testing.T) *Namespace { + ns := NewNamespace("Applications.Compute") + require.Equal(t, NamespaceResourceKind, ns.Kind) + require.Equal(t, "Applications.Compute", ns.Name) + + asyncFunc := func(opts asyncctrl.Options) (asyncctrl.Controller, error) { + return &testAsyncController{}, nil + } + + // register virtualMachines resource + vmResource := ns.AddResource("virtualMachines", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + ResourceParamName: "virtualMachineName", + + RequestConverter: rpctest.TestResourceDataModelFromVersioned, + ResponseConverter: rpctest.TestResourceDataModelToVersioned, + + Put: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + AsyncJobController: asyncFunc, + }, + Patch: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + AsyncJobController: asyncFunc, + }, + Custom: map[string]Operation[rpctest.TestResourceDataModel]{ + "start": { + APIController: newTestController, + AsyncJobController: asyncFunc, + }, + "stop": { + APIController: newTestController, + }, + }, + }) + + require.NotNil(t, vmResource) + + // register virtualMachines/disks child resource + _ = vmResource.AddResource("disks", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + RequestConverter: rpctest.TestResourceDataModelFromVersioned, + ResponseConverter: rpctest.TestResourceDataModelToVersioned, + + Put: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + AsyncJobController: asyncFunc, + }, + Patch: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + AsyncJobController: asyncFunc, + }, + Custom: map[string]Operation[rpctest.TestResourceDataModel]{ + "replace": { + APIController: newTestController, + }, + }, + }) + + // register virtualMachines/networks child resource + _ = vmResource.AddResource("networks", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + RequestConverter: rpctest.TestResourceDataModelFromVersioned, + ResponseConverter: rpctest.TestResourceDataModelToVersioned, + + Put: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + }, + Patch: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + }, + Custom: map[string]Operation[rpctest.TestResourceDataModel]{ + "connect": { + APIController: newTestController, + }, + }, + }) + + // register containers resource + containerResource := ns.AddResource("containers", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + RequestConverter: rpctest.TestResourceDataModelFromVersioned, + ResponseConverter: rpctest.TestResourceDataModelToVersioned, + + Put: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + }, + Patch: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + }, + Custom: map[string]Operation[rpctest.TestResourceDataModel]{ + "getresource": { + APIController: newTestController, + }, + }, + }) + + require.NotNil(t, containerResource) + + // register containers/secrets child resource + _ = containerResource.AddResource("secrets", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + RequestConverter: rpctest.TestResourceDataModelFromVersioned, + ResponseConverter: rpctest.TestResourceDataModelToVersioned, + + Put: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + }, + Patch: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + }, + }) + + // register webAssemblies resource + wasmResource := ns.AddResource("webAssemblies", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + ResourceParamName: "webAssemblyName", + RequestConverter: rpctest.TestResourceDataModelFromVersioned, + ResponseConverter: rpctest.TestResourceDataModelToVersioned, + + Put: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + AsyncJobController: asyncFunc, + }, + Patch: Operation[rpctest.TestResourceDataModel]{ + APIController: newTestController, + AsyncJobController: asyncFunc, + }, + }) + + require.NotNil(t, wasmResource) + + return ns +} + +func TestNamespaceBuild(t *testing.T) { + ns := newTestNamespace(t) + builders := ns.GenerateBuilder() + require.NotNil(t, builders) + + builderTests := []struct { + resourceType string + resourceNamePattern string + path string + method v1.OperationMethod + found bool + }{ + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines", + path: "", + method: "LISTPLANESCOPE", + }, + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines", + path: "", + method: "LIST", + }, + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + path: "", + method: "GET", + }, + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + path: "", + method: "PUT", + }, + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + path: "", + method: "PATCH", + }, + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + path: "", + method: "DELETE", + }, + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + path: "/start", + method: "ACTIONSTART", + }, + { + resourceType: "Applications.Compute/virtualMachines", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + path: "/stop", + method: "ACTIONSTOP", + }, + { + resourceType: "Applications.Compute/virtualMachines/networks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks", + path: "", + method: "LIST", + }, + { + resourceType: "Applications.Compute/virtualMachines/networks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}", + path: "", + method: "GET", + }, + { + resourceType: "Applications.Compute/virtualMachines/networks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}", + path: "", + method: "PUT", + }, + { + resourceType: "Applications.Compute/virtualMachines/networks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}", + path: "", + method: "PATCH", + }, + { + resourceType: "Applications.Compute/virtualMachines/networks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}", + path: "", + method: "DELETE", + }, + { + resourceType: "Applications.Compute/virtualMachines/networks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}", + path: "/connect", + method: "ACTIONCONNECT", + }, + { + resourceType: "Applications.Compute/virtualMachines/disks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks", + path: "", + method: "LIST", + }, + { + resourceType: "Applications.Compute/virtualMachines/disks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}", + path: "", + method: "GET", + }, + { + resourceType: "Applications.Compute/virtualMachines/disks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}", + path: "", + method: "PUT", + }, + { + resourceType: "Applications.Compute/virtualMachines/disks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}", + path: "", + method: "PATCH", + }, + { + resourceType: "Applications.Compute/virtualMachines/disks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}", + path: "", + method: "DELETE", + }, + { + resourceType: "Applications.Compute/virtualMachines/disks", + resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}", + path: "/replace", + method: "ACTIONREPLACE", + }, + { + resourceType: "Applications.Compute/containers", + resourceNamePattern: "applications.compute/containers", + path: "", + method: "LISTPLANESCOPE", + }, + { + resourceType: "Applications.Compute/containers", + resourceNamePattern: "applications.compute/containers", + path: "", + method: "LIST", + }, + { + resourceType: "Applications.Compute/containers", + resourceNamePattern: "applications.compute/containers/{containerName}", + path: "", + method: "GET", + }, + { + resourceType: "Applications.Compute/containers", + resourceNamePattern: "applications.compute/containers/{containerName}", + path: "", + method: "PUT", + }, + { + resourceType: "Applications.Compute/containers", + resourceNamePattern: "applications.compute/containers/{containerName}", + path: "", + method: "PATCH", + }, + { + resourceType: "Applications.Compute/containers", + resourceNamePattern: "applications.compute/containers/{containerName}", + path: "", + method: "DELETE", + }, + { + resourceType: "Applications.Compute/containers", + resourceNamePattern: "applications.compute/containers/{containerName}", + path: "/getresource", + method: "ACTIONGETRESOURCE", + }, + { + resourceType: "Applications.Compute/containers/secrets", + resourceNamePattern: "applications.compute/containers/{containerName}/secrets", + path: "", + method: "LIST", + }, + { + resourceType: "Applications.Compute/containers/secrets", + resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}", + path: "", + method: "GET", + }, + { + resourceType: "Applications.Compute/containers/secrets", + resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}", + path: "", + method: "PUT", + }, + { + resourceType: "Applications.Compute/containers/secrets", + resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}", + path: "", + method: "PATCH", + }, + { + resourceType: "Applications.Compute/containers/secrets", + resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}", + path: "", + method: "DELETE", + }, + { + resourceType: "Applications.Compute/webAssemblies", + resourceNamePattern: "applications.compute/webassemblies", + path: "", + method: "LISTPLANESCOPE", + }, + { + resourceType: "Applications.Compute/webAssemblies", + resourceNamePattern: "applications.compute/webassemblies", + path: "", + method: "LIST", + }, + { + resourceType: "Applications.Compute/webAssemblies", + resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}", + path: "", + method: "GET", + }, + { + resourceType: "Applications.Compute/webAssemblies", + resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}", + path: "", + method: "PUT", + }, + { + resourceType: "Applications.Compute/webAssemblies", + resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}", + path: "", + method: "PATCH", + }, + { + resourceType: "Applications.Compute/webAssemblies", + resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}", + path: "", + method: "DELETE", + }, + } + + for _, b := range builders.registrations { + for i, bt := range builderTests { + if bt.resourceType == b.ResourceType && bt.resourceNamePattern == b.ResourceNamePattern && bt.path == b.Path && bt.method == b.Method { + builderTests[i].found = true + } + } + } + + for _, bt := range builderTests { + require.True(t, bt.found, "resource not found: %s %s %s %s", bt.resourceType, bt.resourceNamePattern, bt.path, bt.method) + } +} diff --git a/pkg/armrpc/builder/node.go b/pkg/armrpc/builder/node.go new file mode 100644 index 0000000000..06c54a493c --- /dev/null +++ b/pkg/armrpc/builder/node.go @@ -0,0 +1,73 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "errors" + "strings" +) + +var ( + // ErrResourceAlreadyExists represents an error when a resource already exists. + ErrResourceAlreadyExists = errors.New("resource already exists") +) + +// ResourceNode is a node in the resource tree. +type ResourceNode struct { + // Kind is the resource kind. + Kind ResourceKind + + // Name is the resource name. + Name string + + // option includes the resource handlers. + option ResourceOptionBuilder + + // children includes the child resources and custom actions of this resource. + children map[string]*ResourceNode +} + +// AddResource adds a new child resource type and API handlers and returns new resource node. +func (r *ResourceNode) AddResource(name string, option ResourceOptionBuilder) *ResourceNode { + normalized := strings.ToLower(name) + + if _, ok := r.children[normalized]; ok { + panic(ErrResourceAlreadyExists) + } + + child := &ResourceNode{ + Name: name, + children: make(map[string]*ResourceNode), + option: option, + } + + switch r.Kind { + case NamespaceResourceKind: + child.Kind = TrackedResourceKind + + case TrackedResourceKind: + child.Kind = ProxyResourceKind + + default: + child.Kind = ProxyResourceKind + } + + option.LinkResource(child) + r.children[normalized] = child + + return child +} diff --git a/pkg/armrpc/builder/node_test.go b/pkg/armrpc/builder/node_test.go new file mode 100644 index 0000000000..865f404198 --- /dev/null +++ b/pkg/armrpc/builder/node_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "testing" + + "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/stretchr/testify/require" +) + +func TestAddResource(t *testing.T) { + r := &ResourceNode{ + Name: "Applications.Core", + Kind: NamespaceResourceKind, + children: make(map[string]*ResourceNode), + } + + child := r.AddResource("virtualMachines", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{}) + require.Equal(t, "virtualMachines", child.Name) + require.Equal(t, TrackedResourceKind, child.Kind, "child resource of namespace should be a tracked resource") + require.Len(t, r.children, 1, "should have one child resource") + + require.Panics(t, func() { + _ = r.AddResource("virtualMachines", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{}) + }, "panic when adding a resource with the same name") + + nested := child.AddResource("disks", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{}) + _ = child.AddResource("cpus", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{}) + require.Equal(t, "disks", nested.Name) + require.Equal(t, ProxyResourceKind, nested.Kind, "nested resource should be a proxy resource") + require.Len(t, child.children, 2, "should have 2 child resource") +} diff --git a/pkg/armrpc/builder/operation.go b/pkg/armrpc/builder/operation.go new file mode 100644 index 0000000000..b491604525 --- /dev/null +++ b/pkg/armrpc/builder/operation.go @@ -0,0 +1,367 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "strings" + "time" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker" + "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation" + "github.com/radius-project/radius/pkg/armrpc/frontend/server" +) + +const customActionPrefix = "ACTION" + +// Operation defines converters for request and response, update and delete filters, +// asynchronous operation controller, and the options for API operation. +type Operation[T any] struct { + // Disabled indicates that the operation is disabled. By default, all operations are enabled. + Disabled bool + + // UpdateFilters is a slice of filters that execute prior to updating a resource. + UpdateFilters []controller.UpdateFilter[T] + + // DeleteFilters is a slice of filters that execute prior to deleting a resource. + DeleteFilters []controller.DeleteFilter[T] + + // APIController is the controller function for the API controller frontend. + APIController server.ControllerFactoryFunc + + // AsyncJobController is the controller function for the async job worker. + AsyncJobController worker.ControllerFactoryFunc + + // AsyncOperationTimeout is the default timeout duration of async operations for the operation. + AsyncOperationTimeout time.Duration + + // AsyncOperationRetryAfter is the value of the Retry-After header that will be used for async operations. + // If this is 0 then the default value of v1.DefaultRetryAfter will be used. Consider setting this to a smaller + // value like 5 seconds if your operations will complete quickly. + AsyncOperationRetryAfter time.Duration +} + +// ResourceOption is the option for ResourceNode. It defines model converters for request and response +// and configures operation for each CRUDL and custom actions. +type ResourceOption[P interface { + *T + v1.ResourceDataModel +}, T any] struct { + // linkedNode references to ResourceNode linked to this option. + linkedNode *ResourceNode + + // ResourceParamName is the parameter name of the resource. This is optional. + // If not set, the parameter name will be generated by adding "Name" suffix to the resource name. + ResourceParamName string + + // RequestConverter is the request converter. + RequestConverter v1.ConvertToDataModel[T] + + // ResponseConverter is the response converter. + ResponseConverter v1.ConvertToAPIModel[T] + + // ListPlane defines the operation for listing resources by plane scope. + ListPlane Operation[T] + + // List defines the operation for listing resources by resource group scope. + List Operation[T] + + // Get defines the operation for getting a resource. + Get Operation[T] + + // Put defines the operation for creating or updating a resource. + Put Operation[T] + + // Patch defines the operation for updating a resource. + Patch Operation[T] + + // Delete defines the operation for deleting a resource. + Delete Operation[T] + + // Custom defines the custom actions. + Custom map[string]Operation[T] +} + +// LinkResource links the resource node to the resource option. +func (r *ResourceOption[P, T]) LinkResource(node *ResourceNode) { + r.linkedNode = node +} + +// ParamName returns the parameter name of the resource. +// If ResourceParamName is not set, the parameter name will be generated by adding "Name" suffix to the resource name. +func (r *ResourceOption[P, T]) ParamName() string { + if r.ResourceParamName == "" { + typeName := r.linkedNode.Name + if strings.HasSuffix(typeName, "s") { + return typeName[:len(typeName)-1] + "Name" + } else { + return typeName + "Name" + } + } + + return r.ResourceParamName +} + +// BuildHandlerOutputs builds the handler outputs for each operation. +func (r *ResourceOption[P, T]) BuildHandlerOutputs(opts BuildOptions) []*OperationRegistration { + handlerFuncs := []func(opts BuildOptions) *OperationRegistration{ + r.listPlaneOutput, + r.listOutput, + r.getOutput, + r.putOutput, + r.patchOutput, + r.deleteOutput, + } + + hs := []*OperationRegistration{} + for _, h := range handlerFuncs { + if out := h(opts); out != nil { + hs = append(hs, out) + } + } + + return append(hs, r.customActionOutputs(opts)...) +} + +func (r *ResourceOption[P, T]) listPlaneOutput(opts BuildOptions) *OperationRegistration { + if r.ListPlane.Disabled || r.linkedNode.Kind != TrackedResourceKind { + return nil + } + + h := &OperationRegistration{ + ResourceType: opts.ResourceType, + ResourceNamePattern: opts.ResourceNamePattern, + Method: v1.OperationPlaneScopeList, + } + + if r.ListPlane.APIController != nil { + h.APIController = r.ListPlane.APIController + } else { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewListResources[P, T](opt, + controller.ResourceOptions[T]{ + ResponseConverter: r.ResponseConverter, + ListRecursiveQuery: true, + }, + ) + } + } + + return h +} + +func (r *ResourceOption[P, T]) listOutput(opts BuildOptions) *OperationRegistration { + if r.List.Disabled { + return nil + } + + h := &OperationRegistration{ + ResourceType: opts.ResourceType, + ResourceNamePattern: opts.ResourceNamePattern, + Method: v1.OperationList, + } + + if r.List.APIController != nil { + h.APIController = r.List.APIController + } else { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewListResources[P, T](opt, + controller.ResourceOptions[T]{ + ResponseConverter: r.ResponseConverter, + }, + ) + } + } + + return h +} + +func (r *ResourceOption[P, T]) getOutput(opts BuildOptions) *OperationRegistration { + if r.Get.Disabled { + return nil + } + + h := &OperationRegistration{ + ResourceType: opts.ResourceType, + ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName, + Method: v1.OperationGet, + } + + if r.Get.APIController != nil { + h.APIController = r.Get.APIController + } else { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewGetResource[P, T](opt, + controller.ResourceOptions[T]{ + ResponseConverter: r.ResponseConverter, + }, + ) + } + } + + return h +} + +func getOrDefaultAsyncOperationTimeout(d time.Duration) time.Duration { + if d == 0 { + return asyncctrl.DefaultAsyncOperationTimeout + } + return d +} + +func getOrDefaultRetryAfter(d time.Duration) time.Duration { + if d == 0 { + return v1.DefaultRetryAfterDuration + } + return d +} + +func (r *ResourceOption[P, T]) putOutput(opts BuildOptions) *OperationRegistration { + if r.Put.Disabled { + return nil + } + + h := &OperationRegistration{ + ResourceType: opts.ResourceType, + ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName, + Method: v1.OperationPut, + AsyncController: r.Delete.AsyncJobController, + } + + if r.Put.APIController != nil { + h.APIController = r.Put.APIController + } else { + ro := controller.ResourceOptions[T]{ + RequestConverter: r.RequestConverter, + ResponseConverter: r.ResponseConverter, + UpdateFilters: r.Put.UpdateFilters, + AsyncOperationTimeout: getOrDefaultAsyncOperationTimeout(r.Put.AsyncOperationTimeout), + AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Put.AsyncOperationRetryAfter), + } + + if r.Put.AsyncJobController == nil { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewDefaultSyncPut[P, T](opt, ro) + } + } else { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewDefaultAsyncPut[P, T](opt, ro) + } + } + } + h.AsyncController = r.Put.AsyncJobController + + return h +} + +func (r *ResourceOption[P, T]) patchOutput(opts BuildOptions) *OperationRegistration { + if r.Patch.Disabled { + return nil + } + + h := &OperationRegistration{ + ResourceType: opts.ResourceType, + ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName, + Method: v1.OperationPatch, + AsyncController: r.Patch.AsyncJobController, + } + + if r.Patch.APIController != nil { + h.APIController = r.Patch.APIController + } else { + ro := controller.ResourceOptions[T]{ + RequestConverter: r.RequestConverter, + ResponseConverter: r.ResponseConverter, + UpdateFilters: r.Patch.UpdateFilters, + AsyncOperationTimeout: getOrDefaultAsyncOperationTimeout(r.Patch.AsyncOperationTimeout), + AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Patch.AsyncOperationRetryAfter), + } + + if r.Patch.AsyncJobController == nil { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewDefaultSyncPut[P, T](opt, ro) + } + } else { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewDefaultAsyncPut[P, T](opt, ro) + } + } + } + + return h +} + +func (r *ResourceOption[P, T]) deleteOutput(opts BuildOptions) *OperationRegistration { + if r.Delete.Disabled { + return nil + } + + h := &OperationRegistration{ + ResourceType: opts.ResourceType, + ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName, + Method: v1.OperationDelete, + AsyncController: r.Delete.AsyncJobController, + } + + if r.Delete.APIController != nil { + h.APIController = r.Delete.APIController + } else { + ro := controller.ResourceOptions[T]{ + RequestConverter: r.RequestConverter, + ResponseConverter: r.ResponseConverter, + DeleteFilters: r.Delete.DeleteFilters, + AsyncOperationTimeout: getOrDefaultAsyncOperationTimeout(r.Delete.AsyncOperationTimeout), + AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Delete.AsyncOperationRetryAfter), + } + + if r.Delete.AsyncJobController == nil { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewDefaultSyncDelete[P, T](opt, ro) + } + } else { + h.APIController = func(opt controller.Options) (controller.Controller, error) { + return defaultoperation.NewDefaultAsyncDelete[P, T](opt, ro) + } + } + } + + return h +} + +func (r *ResourceOption[P, T]) customActionOutputs(opts BuildOptions) []*OperationRegistration { + handlers := []*OperationRegistration{} + + for name, handle := range r.Custom { + if handle.APIController == nil { + panic("APIController is required for custom action") + } + + h := &OperationRegistration{ + ResourceType: opts.ResourceType, + ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName, + Path: "/" + strings.ToLower(name), + Method: v1.OperationMethod(customActionPrefix + strings.ToUpper(name)), + APIController: handle.APIController, + AsyncController: handle.AsyncJobController, + } + handlers = append(handlers, h) + } + + return handlers +} diff --git a/pkg/armrpc/builder/operation_test.go b/pkg/armrpc/builder/operation_test.go new file mode 100644 index 0000000000..4c8141fe19 --- /dev/null +++ b/pkg/armrpc/builder/operation_test.go @@ -0,0 +1,512 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + "errors" + "testing" + "time" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation" + "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/stretchr/testify/require" +) + +var ( + testBuildOptions = BuildOptions{ + ResourceType: "Applications.Compute/virtualMachines", + ResourceNamePattern: "applications.compute/virtualmachines", + } + + testBuildOptionsWithName = BuildOptions{ + ResourceType: "Applications.Compute/virtualMachines", + ResourceNamePattern: "applications.compute/virtualmachines", + ParameterName: "{virtualMachineName}", + } +) + +func TestGetOrDefaultAsyncOperationTimeout(t *testing.T) { + var zeroDuration time.Duration + require.Equal(t, time.Duration(120)*time.Second, getOrDefaultAsyncOperationTimeout(zeroDuration)) + require.Equal(t, time.Duration(1)*time.Minute, getOrDefaultAsyncOperationTimeout(time.Duration(1)*time.Minute)) +} + +func TestGetOrDefaultRetryAfter(t *testing.T) { + var zeroDuration time.Duration + require.Equal(t, time.Duration(60)*time.Second, getOrDefaultRetryAfter(zeroDuration)) + require.Equal(t, time.Duration(1)*time.Minute, getOrDefaultRetryAfter(time.Duration(1)*time.Minute)) +} + +func TestResourceOption_LinkResource(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{} + option.LinkResource(node) + require.Equal(t, node, option.linkedNode) +} + +func TestResourceOption_ParamName(t *testing.T) { + t.Run("custom parameter name", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + ResourceParamName: "virtualMachineName", + } + require.Equal(t, "virtualMachineName", option.ParamName()) + }) + + t.Run("plural resource type name", func(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{} + option.LinkResource(node) + require.Equal(t, "virtualMachineName", option.ParamName()) + }) + + t.Run("plural resource type name without s", func(t *testing.T) { + node := &ResourceNode{Name: "dice", Kind: TrackedResourceKind} + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{} + option.LinkResource(node) + require.Equal(t, "diceName", option.ParamName()) + }) +} + +func TestResourceOption_ListPlaneOutput(t *testing.T) { + t.Run("disabled is true", func(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + ListPlane: Operation[rpctest.TestResourceDataModel]{ + Disabled: true, + }, + } + require.Nil(t, option.listPlaneOutput(BuildOptions{})) + }) + + t.Run("non tracked resource disabled operation", func(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: ProxyResourceKind} + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + ListPlane: Operation[rpctest.TestResourceDataModel]{}, + } + require.Nil(t, option.listPlaneOutput(BuildOptions{})) + }) + + t.Run("custom controller", func(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + ListPlane: Operation[rpctest.TestResourceDataModel]{ + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, errors.New("ok") + }, + }, + } + h := option.listPlaneOutput(testBuildOptions) + require.NotNil(t, h) + _, err := h.APIController(controller.Options{}) + require.EqualError(t, err, "ok") + require.Equal(t, v1.OperationPlaneScopeList, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default controller", func(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + ListPlane: Operation[rpctest.TestResourceDataModel]{}, + } + h := option.listPlaneOutput(testBuildOptions) + require.NotNil(t, h) + require.NotNil(t, h.APIController) + require.Equal(t, v1.OperationPlaneScopeList, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) +} + +func TestResourceOption_ListOutput(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + t.Run("disabled is true", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + List: Operation[rpctest.TestResourceDataModel]{ + Disabled: true, + }, + } + require.Nil(t, option.listOutput(BuildOptions{})) + }) + + t.Run("custom controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + List: Operation[rpctest.TestResourceDataModel]{ + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, errors.New("ok") + }, + }, + } + h := option.listOutput(testBuildOptions) + require.NotNil(t, h) + _, err := h.APIController(controller.Options{}) + require.EqualError(t, err, "ok") + require.Equal(t, v1.OperationList, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + List: Operation[rpctest.TestResourceDataModel]{}, + } + h := option.listOutput(testBuildOptions) + require.NotNil(t, h) + require.NotNil(t, h.APIController) + require.Equal(t, v1.OperationList, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) +} + +func TestResourceOption_GetOutput(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + + t.Run("disabled is true", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Get: Operation[rpctest.TestResourceDataModel]{ + Disabled: true, + }, + } + require.Nil(t, option.getOutput(BuildOptions{})) + }) + + t.Run("custom controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Get: Operation[rpctest.TestResourceDataModel]{ + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, errors.New("ok") + }, + }, + } + h := option.getOutput(testBuildOptionsWithName) + require.NotNil(t, h) + _, err := h.APIController(controller.Options{}) + require.EqualError(t, err, "ok") + require.Equal(t, v1.OperationGet, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Get: Operation[rpctest.TestResourceDataModel]{}, + } + h := option.getOutput(testBuildOptionsWithName) + require.NotNil(t, h) + require.NotNil(t, h.APIController) + require.Equal(t, v1.OperationGet, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) +} + +func TestResourceOption_PutOutput(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + + t.Run("disabled is true", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Put: Operation[rpctest.TestResourceDataModel]{ + Disabled: true, + }, + } + require.Nil(t, option.putOutput(BuildOptions{})) + }) + + t.Run("custom controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Put: Operation[rpctest.TestResourceDataModel]{ + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, errors.New("ok") + }, + }, + } + h := option.putOutput(testBuildOptionsWithName) + require.NotNil(t, h) + _, err := h.APIController(controller.Options{}) + require.EqualError(t, err, "ok") + require.Equal(t, v1.OperationPut, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default sync controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Put: Operation[rpctest.TestResourceDataModel]{}, + } + h := option.putOutput(testBuildOptionsWithName) + require.NotNil(t, h) + require.Equal(t, v1.OperationPut, h.Method) + + api, err := h.APIController(controller.Options{}) + require.NoError(t, err) + _, ok := api.(*defaultoperation.DefaultSyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]) + require.True(t, ok) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default async controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Put: Operation[rpctest.TestResourceDataModel]{ + AsyncJobController: func(opts asyncctrl.Options) (asyncctrl.Controller, error) { + return nil, nil + }, + }, + } + h := option.putOutput(testBuildOptionsWithName) + require.NotNil(t, h) + require.Equal(t, v1.OperationPut, h.Method) + + api, err := h.APIController(controller.Options{}) + require.NoError(t, err) + _, ok := api.(*defaultoperation.DefaultAsyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]) + require.True(t, ok) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) +} + +func TestResourceOption_PatchOutput(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + + t.Run("disabled is true", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Patch: Operation[rpctest.TestResourceDataModel]{ + Disabled: true, + }, + } + require.Nil(t, option.patchOutput(BuildOptions{})) + }) + + t.Run("custom controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Patch: Operation[rpctest.TestResourceDataModel]{ + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, errors.New("ok") + }, + }, + } + h := option.patchOutput(testBuildOptionsWithName) + require.NotNil(t, h) + _, err := h.APIController(controller.Options{}) + require.EqualError(t, err, "ok") + require.Equal(t, v1.OperationPatch, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default sync controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Patch: Operation[rpctest.TestResourceDataModel]{}, + } + h := option.patchOutput(testBuildOptionsWithName) + require.NotNil(t, h) + require.Equal(t, v1.OperationPatch, h.Method) + + api, err := h.APIController(controller.Options{}) + require.NoError(t, err) + _, ok := api.(*defaultoperation.DefaultSyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]) + require.True(t, ok) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default async controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Patch: Operation[rpctest.TestResourceDataModel]{ + AsyncJobController: func(opts asyncctrl.Options) (asyncctrl.Controller, error) { + return nil, nil + }, + }, + } + h := option.patchOutput(testBuildOptionsWithName) + require.NotNil(t, h) + require.Equal(t, v1.OperationPatch, h.Method) + + api, err := h.APIController(controller.Options{}) + require.NoError(t, err) + _, ok := api.(*defaultoperation.DefaultAsyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]) + require.True(t, ok) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) +} + +func TestResourceOption_DeleteOutput(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + + t.Run("disabled is true", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Delete: Operation[rpctest.TestResourceDataModel]{ + Disabled: true, + }, + } + require.Nil(t, option.deleteOutput(BuildOptions{})) + }) + + t.Run("custom controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Delete: Operation[rpctest.TestResourceDataModel]{ + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, errors.New("ok") + }, + }, + } + h := option.deleteOutput(testBuildOptionsWithName) + require.NotNil(t, h) + _, err := h.APIController(controller.Options{}) + require.EqualError(t, err, "ok") + require.Equal(t, v1.OperationDelete, h.Method) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default sync controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Delete: Operation[rpctest.TestResourceDataModel]{}, + } + h := option.deleteOutput(testBuildOptionsWithName) + require.NotNil(t, h) + require.Equal(t, v1.OperationDelete, h.Method) + + api, err := h.APIController(controller.Options{}) + require.NoError(t, err) + _, ok := api.(*defaultoperation.DefaultSyncDelete[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]) + require.True(t, ok) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) + + t.Run("default async controller", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Delete: Operation[rpctest.TestResourceDataModel]{ + AsyncJobController: func(opts asyncctrl.Options) (asyncctrl.Controller, error) { + return nil, nil + }, + }, + } + h := option.deleteOutput(testBuildOptionsWithName) + require.NotNil(t, h) + require.Equal(t, v1.OperationDelete, h.Method) + + api, err := h.APIController(controller.Options{}) + require.NoError(t, err) + _, ok := api.(*defaultoperation.DefaultAsyncDelete[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]) + require.True(t, ok) + require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType) + require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern) + require.Empty(t, h.Path) + }) +} + +func TestResourceOption_CustomActionOutput(t *testing.T) { + node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind} + t.Run("valid custom action", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Custom: map[string]Operation[rpctest.TestResourceDataModel]{ + "start": { + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, nil + }, + }, + "stop": { + APIController: func(opt controller.Options) (controller.Controller, error) { + return nil, nil + }, + }, + }, + } + + hs := option.customActionOutputs(testBuildOptionsWithName) + require.Len(t, hs, 2) + + require.NotNil(t, hs[0].APIController) + require.NotNil(t, hs[1].APIController) + + // Reset APIController to nil for comparison + hs[0].APIController = nil + hs[1].APIController = nil + + require.ElementsMatch(t, []*OperationRegistration{ + { + ResourceType: "Applications.Compute/virtualMachines", + ResourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + Path: "/start", + Method: "ACTIONSTART", + }, + { + ResourceType: "Applications.Compute/virtualMachines", + ResourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}", + Path: "/stop", + Method: "ACTIONSTOP", + }, + }, hs) + }) + + t.Run("APIController is not defined", func(t *testing.T) { + option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{ + linkedNode: node, + Custom: map[string]Operation[rpctest.TestResourceDataModel]{ + "start": {}, + }, + } + require.Panics(t, func() { + _ = option.customActionOutputs(testBuildOptionsWithName) + }) + }) +} diff --git a/pkg/armrpc/builder/types.go b/pkg/armrpc/builder/types.go new file mode 100644 index 0000000000..4b265c964d --- /dev/null +++ b/pkg/armrpc/builder/types.go @@ -0,0 +1,82 @@ +/* +Copyright 2023 The Radius Authors. + +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 builder + +import ( + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker" + "github.com/radius-project/radius/pkg/armrpc/frontend/server" +) + +// ResourceKind represents the kind of resource. +type ResourceKind string + +const ( + // NamespaceResourceKind represents the namespace resource kind. + NamespaceResourceKind ResourceKind = "Namespace" + + // TrackedResourceKind represents the tracked resource kind. + TrackedResourceKind ResourceKind = "TrackedResource" + + // ProxyResourceKind represents the proxy resource kind. + ProxyResourceKind ResourceKind = "ProxyResource" +) + +// ResourceOptionBuilder is the interface for resource option. +type ResourceOptionBuilder interface { + // LinkResource links the resource node to the resource option. + LinkResource(*ResourceNode) + + // ParamName gets the resource name for resource type. + ParamName() string + + // BuildHandlerOutputs builds the resource outputs which constructs the API routing path and handlers. + BuildHandlerOutputs(BuildOptions) []*OperationRegistration +} + +// BuildOptions is the options for building resource outputs. +type BuildOptions struct { + // ResourceType represents the resource type. + ResourceType string + + // ParameterName represents the resource name for resource type. + ParameterName string + + // ResourceNamePattern represents the resource name pattern used for HTTP routing path. + ResourceNamePattern string +} + +// OperationRegistration is the output for building resource outputs. +type OperationRegistration struct { + // ResourceType represents the resource type. + ResourceType string + + // ResourceNamePattern represents the resource name pattern used for HTTP routing path. + ResourceNamePattern string + + // Path represents additional custom action path after resource name. + Path string + + // Method represents the operation method. + Method v1.OperationMethod + + // APIController represents the API controller handler. + APIController server.ControllerFactoryFunc + + // AsyncController represents the async controller handler. + AsyncController worker.ControllerFactoryFunc +} diff --git a/pkg/armrpc/frontend/controller/controller.go b/pkg/armrpc/frontend/controller/controller.go index cea4130853..d7c8bab554 100644 --- a/pkg/armrpc/frontend/controller/controller.go +++ b/pkg/armrpc/frontend/controller/controller.go @@ -61,7 +61,7 @@ type Options struct { // KubeClient is the Kubernetes controller runtime client. KubeClient runtimeclient.Client - // ResourceType is the string that represents the resource type. May be empty of the controller + // ResourceType is the string that represents the resource type. May be empty if the controller // does not represent a single type of resource. ResourceType string diff --git a/pkg/armrpc/frontend/defaultoperation/getavailableoperations.go b/pkg/armrpc/frontend/defaultoperation/getavailableoperations.go new file mode 100644 index 0000000000..6dcc1bab5b --- /dev/null +++ b/pkg/armrpc/frontend/defaultoperation/getavailableoperations.go @@ -0,0 +1,50 @@ +/* +Copyright 2023 The Radius Authors. + +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 defaultoperation + +import ( + "context" + "net/http" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/rest" +) + +var _ ctrl.Controller = (*GetOperations)(nil) + +// GetOperations is the controller implementation to get arm rpc available operations. +type GetOperations struct { + ctrl.BaseController + + availableOperations []any +} + +// NewGetOperations creates a new GetOperations controller and returns it, or returns an error if one occurs. +func NewGetOperations(opts ctrl.Options, opsList []v1.Operation) (ctrl.Controller, error) { + ops := []any{} + for _, o := range opsList { + ops = append(ops, o) + } + return &GetOperations{ctrl.NewBaseController(opts), ops}, nil +} + +// Run returns the list of available operations/permission for the resource provider at tenant level. +// Spec: https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/proxy-api-reference.md#exposing-available-operations +func (opctrl *GetOperations) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) { + return rest.NewOKResponse(&v1.PaginatedList{Value: opctrl.availableOperations}), nil +} diff --git a/pkg/armrpc/frontend/defaultoperation/getoperationresult.go b/pkg/armrpc/frontend/defaultoperation/getoperationresult.go index bf2abbc946..e5f64effd8 100644 --- a/pkg/armrpc/frontend/defaultoperation/getoperationresult.go +++ b/pkg/armrpc/frontend/defaultoperation/getoperationresult.go @@ -56,12 +56,24 @@ func (e *GetOperationResult) Run(ctx context.Context, w http.ResponseWriter, req return rest.NewBadRequestResponse(err.Error()), nil } - os := &manager.Status{} - _, err = e.GetResource(ctx, id.String(), os) - if err != nil && errors.Is(&store.ErrNotFound{ID: id.String()}, err) { + // Avoid using GetResource or e.StorageClient since they will use a different + // storage client than the one we want. + storageClient, err := e.DataProvider().GetStorageClient(ctx, id.ProviderNamespace()+"/operationstatuses") + if err != nil { + return nil, err + } + + obj, err := storageClient.Get(ctx, id.String()) + if errors.Is(&store.ErrNotFound{ID: id.String()}, err) { return rest.NewNotFoundResponse(id), nil } + os := &manager.Status{} + err = obj.As(os) + if err != nil { + return nil, err + } + if !os.Status.IsTerminal() { headers := map[string]string{ "Location": req.URL.String(), diff --git a/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go b/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go index e9276d806c..978e9f42aa 100644 --- a/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go +++ b/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go @@ -28,7 +28,9 @@ import ( manager "github.com/radius-project/radius/pkg/armrpc/asyncoperation/statusmanager" ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/radius-project/radius/pkg/ucp/dataprovider" "github.com/radius-project/radius/pkg/ucp/store" + "github.com/radius-project/radius/test/testcontext" "github.com/radius-project/radius/test/testutil" "github.com/golang/mock/gomock" @@ -36,27 +38,34 @@ import ( ) func TestGetOperationResultRun(t *testing.T) { - mctrl := gomock.NewController(t) - defer mctrl.Finish() - - mStorageClient := store.NewMockStorageClient(mctrl) - ctx := context.Background() - rawDataModel := testutil.ReadFixture("operationstatus_datamodel.json") osDataModel := &manager.Status{} - _ = json.Unmarshal(rawDataModel, osDataModel) + err := json.Unmarshal(rawDataModel, osDataModel) + require.NoError(t, err) rawExpectedOutput := testutil.ReadFixture("operationstatus_output.json") expectedOutput := &v1.AsyncOperationStatus{} - _ = json.Unmarshal(rawExpectedOutput, expectedOutput) + err = json.Unmarshal(rawExpectedOutput, expectedOutput) + require.NoError(t, err) t.Run("get non-existing resource", func(t *testing.T) { + mctrl := gomock.NewController(t) + + operationResultStoreClient := store.NewMockStorageClient(mctrl) + operationStatusStoreClient := store.NewMockStorageClient(mctrl) + + dataProvider := dataprovider.NewMockDataStorageProvider(mctrl) + dataProvider.EXPECT(). + GetStorageClient(gomock.Any(), "Applications.Core/operationstatuses"). + Return(operationStatusStoreClient, nil). + Times(1) + w := httptest.NewRecorder() - req, err := rpctest.NewHTTPRequestFromJSON(ctx, http.MethodGet, operationStatusTestHeaderFile, nil) + req, err := rpctest.NewHTTPRequestFromJSON(testcontext.New(t), http.MethodGet, operationStatusTestHeaderFile, nil) require.NoError(t, err) ctx := rpctest.NewARMRequestContext(req) - mStorageClient. + operationStatusStoreClient. EXPECT(). Get(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { @@ -64,7 +73,8 @@ func TestGetOperationResultRun(t *testing.T) { }) ctl, err := NewGetOperationResult(ctrl.Options{ - StorageClient: mStorageClient, + DataProvider: dataProvider, + StorageClient: operationResultStoreClient, // Will not be used. }) require.NoError(t, err) @@ -114,15 +124,25 @@ func TestGetOperationResultRun(t *testing.T) { for _, tt := range opResTestCases { t.Run(tt.desc, func(t *testing.T) { + mctrl := gomock.NewController(t) + operationResultStoreClient := store.NewMockStorageClient(mctrl) + operationStatusStoreClient := store.NewMockStorageClient(mctrl) + + dataProvider := dataprovider.NewMockDataStorageProvider(mctrl) + dataProvider.EXPECT(). + GetStorageClient(gomock.Any(), "Applications.Core/operationstatuses"). + Return(operationStatusStoreClient, nil). + Times(1) + w := httptest.NewRecorder() - req, err := rpctest.NewHTTPRequestFromJSON(ctx, http.MethodGet, operationStatusTestHeaderFile, nil) + req, err := rpctest.NewHTTPRequestFromJSON(testcontext.New(t), http.MethodGet, operationStatusTestHeaderFile, nil) require.NoError(t, err) ctx := rpctest.NewARMRequestContext(req) osDataModel.Status = tt.provisioningState osDataModel.RetryAfter = time.Second * 5 - mStorageClient. + operationStatusStoreClient. EXPECT(). Get(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { @@ -133,7 +153,8 @@ func TestGetOperationResultRun(t *testing.T) { }) ctl, err := NewGetOperationResult(ctrl.Options{ - StorageClient: mStorageClient, + DataProvider: dataProvider, + StorageClient: operationResultStoreClient, // Will not be used. }) require.NoError(t, err) diff --git a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json index 95731f8d21..83bfbf97a2 100644 --- a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json +++ b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json @@ -5,8 +5,8 @@ "operationType": "PUT", "location": "West US", "status": "Succeeded", - "startTime": "2022-05-16T10:24:58+0000", - "endTime": "2022-05-16T17:24:58+0000", + "startTime": "2022-05-16T10:24:58.000000Z", + "endTime": "2022-05-16T17:24:58.000000Z", "percentComplete": "100", "properties": { "provisioningState": "Succeeded" diff --git a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json index 65785327d9..42602e8d6f 100644 --- a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json +++ b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json @@ -5,8 +5,8 @@ "operationType": "PUT", "location": "West US", "status": "Succeeded", - "startTime": "2022-05-16T10:24:58+0000", - "endTime": "2022-05-16T17:24:58+0000", + "startTime": "2022-05-16T10:24:58.000000Z", + "endTime": "2022-05-16T17:24:58.000000Z", "percentComplete": "100", "properties": { "provisioningState": "Succeeded" diff --git a/pkg/armrpc/frontend/server/handler.go b/pkg/armrpc/frontend/server/handler.go index 612c150bd2..a32279f734 100644 --- a/pkg/armrpc/frontend/server/handler.go +++ b/pkg/armrpc/frontend/server/handler.go @@ -29,7 +29,6 @@ import ( ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation" "github.com/radius-project/radius/pkg/armrpc/rest" - "github.com/radius-project/radius/pkg/armrpc/servicecontext" "github.com/radius-project/radius/pkg/ucp/ucplog" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" @@ -47,7 +46,7 @@ var ( ErrInvalidOperationTypeOption = errors.New("the resource type and method must be specified if the operation type is not specified") ) -type ControllerFunc func(ctrl.Options) (ctrl.Controller, error) +type ControllerFactoryFunc func(ctrl.Options) (ctrl.Controller, error) // HandlerOptions represents a controller to be registered with the server. // @@ -85,7 +84,7 @@ type HandlerOptions struct { OperationType *v1.OperationType // ControllerFactory is a function invoked to create the controller. Will be invoked once during server startup. - ControllerFactory ControllerFunc + ControllerFactory ControllerFactoryFunc // Middlewares are the middlewares to apply to the handler. Middlewares []func(http.Handler) http.Handler @@ -101,10 +100,16 @@ func NewSubrouter(parent chi.Router, path string, middlewares ...func(http.Handl // HandlerForController creates a http.HandlerFunc function that runs resource provider frontend controller, renders a // http response from the returned rest.Response, and handles the error as a default internal error if this controller returns error. -func HandlerForController(controller ctrl.Controller) http.HandlerFunc { +func HandlerForController(controller ctrl.Controller, operationType v1.OperationType) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { ctx := req.Context() - addRequestAttributes(ctx, req) + + rpcCtx := v1.ARMRequestContextFromContext(ctx) + // Set the operation type in the context. + rpcCtx.OperationType = operationType + + // Add OTEL labels for the telemetry. + withOtelLabelsForRequest(req) response, err := controller.Run(ctx, w, req) if err != nil { @@ -159,9 +164,8 @@ func RegisterHandler(ctx context.Context, opts HandlerOptions, ctrlOpts ctrl.Opt return nil } - middlewares := append(opts.Middlewares, servicecontext.WithOperationType(*opts.OperationType)) - handler := HandlerForController(ctrl) - namedRouter := opts.ParentRouter.With(middlewares...) + handler := HandlerForController(ctrl, *opts.OperationType) + namedRouter := opts.ParentRouter.With(opts.Middlewares...) if opts.Path == CatchAllPath { namedRouter.HandleFunc(opts.Path, handler) } else { @@ -171,13 +175,13 @@ func RegisterHandler(ctx context.Context, opts HandlerOptions, ctrlOpts ctrl.Opt return nil } -func addRequestAttributes(ctx context.Context, req *http.Request) { - labeler, ok := otelhttp.LabelerFromContext(ctx) +func withOtelLabelsForRequest(req *http.Request) { + labeler, ok := otelhttp.LabelerFromContext(req.Context()) if !ok { return } - armContext := v1.ARMRequestContextFromContext(ctx) + armContext := v1.ARMRequestContextFromContext(req.Context()) resourceID := armContext.ResourceID if resourceID.IsResource() || resourceID.IsResourceCollection() { @@ -193,7 +197,7 @@ func ConfigureDefaultHandlers( rootScopePath string, isAzureProvider bool, providerNamespace string, - operationCtrlFactory ControllerFunc, + operationCtrlFactory ControllerFactoryFunc, ctrlOpts ctrl.Options) error { providerNamespace = strings.ToLower(providerNamespace) rt := providerNamespace + "/providers" @@ -230,7 +234,7 @@ func ConfigureDefaultHandlers( ParentRouter: rootRouter, Path: opStatus, ResourceType: statusRT, - Method: v1.OperationGetOperationStatuses, + Method: v1.OperationGet, ControllerFactory: defaultoperation.NewGetOperationStatus, }, ctrlOpts) if err != nil { @@ -242,7 +246,7 @@ func ConfigureDefaultHandlers( ParentRouter: rootRouter, Path: opResult, ResourceType: statusRT, - Method: v1.OperationGetOperationResult, + Method: v1.OperationGet, ControllerFactory: defaultoperation.NewGetOperationResult, }, ctrlOpts) if err != nil { diff --git a/pkg/armrpc/frontend/server/handler_test.go b/pkg/armrpc/frontend/server/handler_test.go index db7b005f24..d052c0be40 100644 --- a/pkg/armrpc/frontend/server/handler_test.go +++ b/pkg/armrpc/frontend/server/handler_test.go @@ -17,6 +17,7 @@ limitations under the License. package server import ( + "bytes" "context" "encoding/json" "errors" @@ -29,6 +30,8 @@ import ( "github.com/golang/mock/gomock" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/rest" + "github.com/radius-project/radius/pkg/armrpc/rpctest" "github.com/radius-project/radius/pkg/middleware" "github.com/radius-project/radius/pkg/ucp/dataprovider" "github.com/radius-project/radius/test/testcontext" @@ -261,3 +264,28 @@ func Test_HandlerErrInternal(t *testing.T) { require.Equal(t, v1.CodeInternal, armerr.Error.Code) require.Equal(t, armerr.Error.Message, "Internal error") } + +type testAPIController struct { + ctrl.Operation[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel] +} + +func (e *testAPIController) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) { + return nil, nil +} + +func Test_HandlerForController_OperationType(t *testing.T) { + expectedType := v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: "GET"} + + handler := HandlerForController(&testAPIController{}, expectedType) + w := httptest.NewRecorder() + + req, err := http.NewRequest(http.MethodGet, "", bytes.NewBuffer([]byte{})) + require.NoError(t, err) + + rCtx := &v1.ARMRequestContext{} + req = req.WithContext(v1.WithARMRequestContext(context.Background(), rCtx)) + + handler.ServeHTTP(w, req) + + require.Equal(t, expectedType.String(), rCtx.OperationType.String()) +} diff --git a/pkg/armrpc/frontend/server/server.go b/pkg/armrpc/frontend/server/server.go index b69895580a..95e5550333 100644 --- a/pkg/armrpc/frontend/server/server.go +++ b/pkg/armrpc/frontend/server/server.go @@ -38,13 +38,13 @@ const ( ) type Options struct { - ProviderNamespace string - Location string - Address string - PathBase string - EnableArmAuth bool - Configure func(chi.Router) error - ArmCertMgr *authentication.ArmCertManager + ServiceName string + Location string + Address string + PathBase string + EnableArmAuth bool + Configure func(chi.Router) error + ArmCertMgr *authentication.ArmCertManager } // New creates a frontend server that can listen on the provided address and serve requests - it creates an HTTP server with a router, @@ -54,7 +54,7 @@ func New(ctx context.Context, options Options) (*http.Server, error) { r := chi.NewRouter() r.Use(middleware.Recoverer) - r.Use(middleware.WithLogger(options.ProviderNamespace)) + r.Use(middleware.WithLogger) r.NotFound(validator.APINotFoundHandler()) r.MethodNotAllowed(validator.APIMethodNotAllowedHandler()) @@ -77,7 +77,7 @@ func New(ctx context.Context, options Options) (*http.Server, error) { handlerFunc := otelhttp.NewHandler( middleware.LowercaseURLPath(r), - options.ProviderNamespace, + options.ServiceName, otelhttp.WithMeterProvider(otel.GetMeterProvider()), otelhttp.WithTracerProvider(otel.GetTracerProvider())) diff --git a/pkg/armrpc/frontend/server/service.go b/pkg/armrpc/frontend/server/service.go index 9a51ac2dc3..f46244cfbe 100644 --- a/pkg/armrpc/frontend/server/service.go +++ b/pkg/armrpc/frontend/server/service.go @@ -58,17 +58,12 @@ func (s *Service) Init(ctx context.Context) error { logger := ucplog.FromContextOrDiscard(ctx) s.StorageProvider = dataprovider.NewStorageProvider(s.Options.Config.StorageProvider) - qp := qprovider.New(s.ProviderName, s.Options.Config.QueueProvider) - opSC, err := s.StorageProvider.GetStorageClient(ctx, s.ProviderName+"/operationstatuses") - if err != nil { - return err - } + qp := qprovider.New(s.Options.Config.QueueProvider) reqQueueClient, err := qp.GetClient(ctx) if err != nil { return err } - s.OperationStatusManager = manager.New(opSC, reqQueueClient, s.ProviderName, s.Options.Config.Env.RoleLocation) - + s.OperationStatusManager = manager.New(s.StorageProvider, reqQueueClient, s.Options.Config.Env.RoleLocation) s.KubeClient, err = kubeutil.NewRuntimeClient(s.Options.K8sConfig) if err != nil { return err diff --git a/pkg/armrpc/rpctest/routers.go b/pkg/armrpc/rpctest/routers.go index 73300eb1f1..4838eed556 100644 --- a/pkg/armrpc/rpctest/routers.go +++ b/pkg/armrpc/rpctest/routers.go @@ -52,9 +52,9 @@ func AssertRouters(t *testing.T, tests []HandlerTestSpec, pathBase, rootScope st r, err := configureRouter(ctx) require.NoError(t, err) - t.Log("Avaiable routes:") + t.Log("Available routes:") err = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { - t.Logf("Method: %s, Path: %s", method, route) + t.Logf("Method: %s, Path: %s, Middlewares: %+v", method, route, middlewares) return nil }) require.NoError(t, err) @@ -86,35 +86,48 @@ func AssertRouters(t *testing.T, tests []HandlerTestSpec, pathBase, rootScope st if tt.SkipOperationTypeValidation { return } + }) + } +} + +// AssertRequests asserts that the restful APIs matches the routes and its operation type matches the given test cases. +// This is working only for test controllers. If you want to validate the routes for the real controllers, use AssertRouters. +func AssertRequests(t *testing.T, tests []HandlerTestSpec, pathBase, rootScope string, configureRouter func(context.Context) (chi.Router, error)) { + ctx := testcontext.New(t) + r, err := configureRouter(ctx) + require.NoError(t, err) + + t.Log("Available routes:") + err = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + t.Logf("Method: %s, Path: %s, Middlewares: %+v", method, route, middlewares) + return nil + }) + require.NoError(t, err) + + for _, tt := range tests { + pb := "" + if !tt.SkipPathBase { + pb = pathBase + } - err = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { - if tctx.RoutePattern() == route && tt.Method == method { - found := false - for _, m := range middlewares { - w := httptest.NewRecorder() - - // It will not validate body. - req, err := http.NewRequest(tt.Method, uri, bytes.NewBuffer([]byte{})) - require.NoError(t, err) - - rCtx := &v1.ARMRequestContext{} - req = req.WithContext(v1.WithARMRequestContext(context.Background(), rCtx)) - - // Pass empty router to validate operation type. - testr := chi.NewRouter() - m(testr).ServeHTTP(w, req) - if tt.OperationType.String() == rCtx.OperationType.String() { - t.Log("Found operation type") - found = true - break - } - } - require.True(t, found, "operation type not found") - } - return nil - }) + uri := pb + rootScope + tt.Path + if tt.WithoutRootScope { + uri = pb + tt.Path + } + + t.Run(tt.Method+"|"+tt.Path, func(t *testing.T) { + w := httptest.NewRecorder() + // It will not validate body. + req, err := http.NewRequest(tt.Method, uri, bytes.NewBuffer([]byte{})) require.NoError(t, err) + + rCtx := &v1.ARMRequestContext{} + req = req.WithContext(v1.WithARMRequestContext(context.Background(), rCtx)) + + r.ServeHTTP(w, req) + require.NotEqual(t, 404, w.Result().StatusCode) + require.Equal(t, tt.OperationType.String(), rCtx.OperationType.String(), "operation type not found: %s %s %s", uri, tt.Method, rCtx.OperationType.String()) }) } } diff --git a/pkg/armrpc/rpctest/testdatamodel.go b/pkg/armrpc/rpctest/testdatamodel.go new file mode 100644 index 0000000000..bc6183bec0 --- /dev/null +++ b/pkg/armrpc/rpctest/testdatamodel.go @@ -0,0 +1,166 @@ +/* +Copyright 2023 The Radius Authors. + +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 rpctest + +import ( + "encoding/json" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/to" +) + +const ( + TestAPIVersion = "2022-03-15-privatepreview" +) + +// TestResourceDataModel represents test resource. +type TestResourceDataModel struct { + v1.BaseResource + + // Properties is the properties of the resource. + Properties *TestResourceDataModelProperties `json:"properties"` +} + +// ResourceTypeName returns the qualified name of the resource +func (r *TestResourceDataModel) ResourceTypeName() string { + return "Applications.Core/resources" +} + +// TestResourceDataModelProperties represents the properties of TestResourceDataModel. +type TestResourceDataModelProperties struct { + Application string `json:"application"` + Environment string `json:"environment"` + PropertyA string `json:"propertyA,omitempty"` + PropertyB string `json:"propertyB,omitempty"` +} + +// TestResource represents test resource for api version. +type TestResource struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + SystemData *v1.SystemData `json:"systemData,omitempty"` + Type *string `json:"type,omitempty"` + Location *string `json:"location,omitempty"` + Properties *TestResourceProperties `json:"properties,omitempty"` + Tags map[string]*string `json:"tags,omitempty"` +} + +// TestResourceProperties - HTTP Route properties +type TestResourceProperties struct { + ProvisioningState *v1.ProvisioningState `json:"provisioningState,omitempty"` + Environment *string `json:"environment,omitempty"` + Application *string `json:"application,omitempty"` + PropertyA *string `json:"propertyA,omitempty"` + PropertyB *string `json:"propertyB,omitempty"` +} + +// # Function Explanation +// +// ConvertTo converts a version specific TestResource into a version-agnostic resource, TestResourceDataModel. +func (src *TestResource) ConvertTo() (v1.DataModelInterface, error) { + converted := &TestResourceDataModel{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + ID: to.String(src.ID), + Name: to.String(src.Name), + Type: to.String(src.Type), + Location: to.String(src.Location), + Tags: to.StringMap(src.Tags), + }, + InternalMetadata: v1.InternalMetadata{ + UpdatedAPIVersion: TestAPIVersion, + AsyncProvisioningState: toProvisioningStateDataModel(src.Properties.ProvisioningState), + }, + }, + Properties: &TestResourceDataModelProperties{ + Application: to.String(src.Properties.Application), + Environment: to.String(src.Properties.Environment), + PropertyA: to.String(src.Properties.PropertyA), + PropertyB: to.String(src.Properties.PropertyB), + }, + } + return converted, nil +} + +// # Function Explanation +// +// ConvertFrom converts src version agnostic model to versioned model, TestResource. +func (dst *TestResource) ConvertFrom(src v1.DataModelInterface) error { + dm, ok := src.(*TestResourceDataModel) + if !ok { + return v1.ErrInvalidModelConversion + } + + dst.ID = to.Ptr(dm.ID) + dst.Name = to.Ptr(dm.Name) + dst.Type = to.Ptr(dm.Type) + dst.SystemData = &dm.SystemData + dst.Location = to.Ptr(dm.Location) + dst.Tags = *to.StringMapPtr(dm.Tags) + dst.Properties = &TestResourceProperties{ + ProvisioningState: fromProvisioningStateDataModel(dm.InternalMetadata.AsyncProvisioningState), + Environment: to.Ptr(dm.Properties.Environment), + Application: to.Ptr(dm.Properties.Application), + PropertyA: to.Ptr(dm.Properties.PropertyA), + PropertyB: to.Ptr(dm.Properties.PropertyB), + } + + return nil +} + +func toProvisioningStateDataModel(state *v1.ProvisioningState) v1.ProvisioningState { + if state == nil { + return v1.ProvisioningStateAccepted + } + return *state +} + +func fromProvisioningStateDataModel(state v1.ProvisioningState) *v1.ProvisioningState { + converted := v1.ProvisioningStateAccepted + if state != "" { + converted = state + } + + return &converted +} + +func TestResourceDataModelToVersioned(model *TestResourceDataModel, version string) (v1.VersionedModelInterface, error) { + switch version { + case TestAPIVersion: + versioned := &TestResource{} + err := versioned.ConvertFrom(model) + return versioned, err + + default: + return nil, v1.ErrUnsupportedAPIVersion + } +} + +func TestResourceDataModelFromVersioned(content []byte, version string) (*TestResourceDataModel, error) { + switch version { + case TestAPIVersion: + am := &TestResource{} + if err := json.Unmarshal(content, am); err != nil { + return nil, err + } + dm, err := am.ConvertTo() + return dm.(*TestResourceDataModel), err + + default: + return nil, v1.ErrUnsupportedAPIVersion + } +} diff --git a/pkg/corerp/backend/service.go b/pkg/corerp/backend/service.go index e072b609b2..6b5dbc5986 100644 --- a/pkg/corerp/backend/service.go +++ b/pkg/corerp/backend/service.go @@ -30,6 +30,7 @@ import ( "github.com/radius-project/radius/pkg/corerp/datamodel" "github.com/radius-project/radius/pkg/corerp/model" "github.com/radius-project/radius/pkg/corerp/processors/extenders" + "github.com/radius-project/radius/pkg/kubeutil" "github.com/radius-project/radius/pkg/portableresources" pr_backend_ctrl "github.com/radius-project/radius/pkg/portableresources/backend/controller" "github.com/radius-project/radius/pkg/portableresources/processors" @@ -84,16 +85,21 @@ func (w *Service) Run(ctx context.Context) error { return err } - coreAppModel, err := model.NewApplicationModel(w.Options.Arm, w.KubeClient, w.KubeClientSet, w.KubeDiscoveryClient, w.KubeDynamicClientSet) + k8s, err := kubeutil.NewClients(w.Options.K8sConfig) + if err != nil { + return fmt.Errorf("failed to initialize kubernetes clients: %w", err) + } + + coreAppModel, err := model.NewApplicationModel(w.Options.Arm, k8s.RuntimeClient, k8s.ClientSet, k8s.DiscoveryClient, k8s.DynamicClient) if err != nil { return fmt.Errorf("failed to initialize application model: %w", err) } opts := ctrl.Options{ DataProvider: w.StorageProvider, - KubeClient: w.KubeClient, + KubeClient: k8s.RuntimeClient, GetDeploymentProcessor: func() deployment.DeploymentProcessor { - return deployment.NewDeploymentProcessor(coreAppModel, w.StorageProvider, w.KubeClient, w.KubeClientSet) + return deployment.NewDeploymentProcessor(coreAppModel, w.StorageProvider, k8s.RuntimeClient, k8s.ClientSet) }, } @@ -113,7 +119,7 @@ func (w *Service) Run(ctx context.Context) error { } } - client := processors.NewResourceClient(w.Options.Arm, w.Options.UCPConnection, w.KubeClient, w.KubeDiscoveryClient) + client := processors.NewResourceClient(w.Options.Arm, w.Options.UCPConnection, k8s.RuntimeClient, k8s.DiscoveryClient) clientOptions := sdk.NewClientOptions(w.Options.UCPConnection) deploymentEngineClient, err := clients.NewResourceDeploymentsClient(&clients.Options{ @@ -133,18 +139,18 @@ func (w *Service) Run(ctx context.Context) error { recipes.TemplateKindTerraform: driver.NewTerraformDriver(w.Options.UCPConnection, provider.NewSecretProvider(w.Options.Config.SecretProvider), driver.TerraformOptions{ Path: w.Options.Config.Terraform.Path, - }, w.KubeClientSet), + }, k8s.ClientSet), }, }) opts.GetDeploymentProcessor = nil extenderCreateOrUpdateController := func(options ctrl.Options) (ctrl.Controller, error) { processor := &extenders.Processor{} - return pr_backend_ctrl.NewCreateOrUpdateResource[*datamodel.Extender, datamodel.Extender](processor, engine, client, configLoader, options) + return pr_backend_ctrl.NewCreateOrUpdateResource[*datamodel.Extender, datamodel.Extender](options, processor, engine, client, configLoader) } extenderDeleteController := func(options ctrl.Options) (ctrl.Controller, error) { processor := &extenders.Processor{} - return pr_backend_ctrl.NewDeleteResource[*datamodel.Extender, datamodel.Extender](processor, engine, configLoader, options) + return pr_backend_ctrl.NewDeleteResource[*datamodel.Extender, datamodel.Extender](options, processor, engine, configLoader) } // Register controllers to run backend processing for extenders. diff --git a/pkg/corerp/frontend/handler/getoperations_test.go b/pkg/corerp/frontend/handler/getoperations_test.go deleted file mode 100644 index d43066d618..0000000000 --- a/pkg/corerp/frontend/handler/getoperations_test.go +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -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 handler - -import ( - "context" - "net/http/httptest" - "testing" - - v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" - ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" - "github.com/radius-project/radius/pkg/armrpc/rest" - v20220315privatepreview "github.com/radius-project/radius/pkg/corerp/api/v20220315privatepreview" - "github.com/stretchr/testify/require" -) - -func TestRunWith20220315PrivatePreview(t *testing.T) { - // arrange - opts := ctrl.Options{} - op, err := NewGetOperations(opts) - require.NoError(t, err) - ctx := v1.WithARMRequestContext(context.Background(), &v1.ARMRequestContext{ - APIVersion: v20220315privatepreview.Version, - }) - w := httptest.NewRecorder() - - // act - resp, err := op.Run(ctx, w, nil) - - // assert - require.NoError(t, err) - switch v := resp.(type) { - case *rest.OKResponse: - pagination, ok := v.Body.(*v1.PaginatedList) - require.True(t, ok) - require.Equal(t, 24, len(pagination.Value)) - default: - require.Truef(t, false, "should not return error") - } -} - -func TestRunWithUnsupportedAPIVersion(t *testing.T) { - // arrange - opts := ctrl.Options{} - op, err := NewGetOperations(opts) - require.NoError(t, err) - ctx := v1.WithARMRequestContext(context.Background(), &v1.ARMRequestContext{ - APIVersion: "unknownversion", - }) - w := httptest.NewRecorder() - - // act - resp, err := op.Run(ctx, w, nil) - - // assert - require.NoError(t, err) - switch v := resp.(type) { - case *rest.NotFoundResponse: - armerr := v.Body - require.Equal(t, v1.CodeInvalidResourceType, armerr.Error.Code) - default: - require.Truef(t, false, "should not return error") - } -} diff --git a/pkg/corerp/frontend/handler/routes_test.go b/pkg/corerp/frontend/handler/routes_test.go index 22ab8b1392..735b57091d 100644 --- a/pkg/corerp/frontend/handler/routes_test.go +++ b/pkg/corerp/frontend/handler/routes_test.go @@ -217,11 +217,11 @@ var handlerTests = []rpctest.HandlerTestSpec{ Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0", Method: http.MethodDelete, }, { - OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGetOperationStatuses}, + OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGet}, Path: "/providers/applications.core/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000", Method: http.MethodGet, }, { - OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGetOperationResult}, + OperationType: v1.OperationType{Type: "Applications.Core/operationResults", Method: v1.OperationGet}, Path: "/providers/applications.core/locations/global/operationresults/00000000-0000-0000-0000-000000000000", Method: http.MethodGet, }, diff --git a/pkg/corerp/frontend/service.go b/pkg/corerp/frontend/service.go deleted file mode 100644 index 07125565ab..0000000000 --- a/pkg/corerp/frontend/service.go +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -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 frontend - -import ( - "context" - "fmt" - - "github.com/go-chi/chi/v5" - ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" - "github.com/radius-project/radius/pkg/armrpc/frontend/server" - "github.com/radius-project/radius/pkg/armrpc/hostoptions" - "github.com/radius-project/radius/pkg/corerp/frontend/handler" - "github.com/radius-project/radius/pkg/recipes" - "github.com/radius-project/radius/pkg/recipes/driver" - "github.com/radius-project/radius/pkg/recipes/engine" - "github.com/radius-project/radius/pkg/sdk" - "github.com/radius-project/radius/pkg/ucp/secret/provider" -) - -type Service struct { - server.Service -} - -// NewService creates a new Service instance with the given options. -func NewService(options hostoptions.HostOptions) *Service { - return &Service{ - server.Service{ - Options: options, - ProviderName: handler.ProviderNamespaceName, - }, - } -} - -// Name returns the namespace of the resource provider. -func (s *Service) Name() string { - return handler.ProviderNamespaceName -} - -// Run initializes the service and starts the server with the specified options. -func (s *Service) Run(ctx context.Context) error { - if err := s.Init(ctx); err != nil { - return err - } - - // Creates a new engine with the drivers. The engine will be used to fetch Recipe parameter information from the template path. - clientOptions := sdk.NewClientOptions(s.Options.UCPConnection) - engine := engine.NewEngine(engine.Options{ - Drivers: map[string]driver.Driver{ - recipes.TemplateKindBicep: driver.NewBicepDriver(clientOptions, nil, nil), - recipes.TemplateKindTerraform: driver.NewTerraformDriver(s.Options.UCPConnection, provider.NewSecretProvider(s.Options.Config.SecretProvider), - driver.TerraformOptions{ - Path: s.Options.Config.Terraform.Path, - }, nil), - }, - }) - - opts := ctrl.Options{ - Address: fmt.Sprintf("%s:%d", s.Options.Config.Server.Host, s.Options.Config.Server.Port), - PathBase: s.Options.Config.Server.PathBase, - DataProvider: s.StorageProvider, - KubeClient: s.KubeClient, - StatusManager: s.OperationStatusManager, - } - - err := s.Start(ctx, server.Options{ - Address: opts.Address, - ProviderNamespace: s.ProviderName, - Location: s.Options.Config.Env.RoleLocation, - PathBase: s.Options.Config.Server.PathBase, - // set the arm cert manager for managing client certificate - ArmCertMgr: s.ARMCertManager, - EnableArmAuth: s.Options.Config.Server.EnableArmAuth, // when enabled the client cert validation will be done - Configure: func(router chi.Router) error { - err := handler.AddRoutes(ctx, router, !hostoptions.IsSelfHosted(), opts, engine) - if err != nil { - return err - } - - return nil - }}, - ) - return err -} diff --git a/pkg/corerp/setup/operations.go b/pkg/corerp/setup/operations.go new file mode 100644 index 0000000000..d132ae4800 --- /dev/null +++ b/pkg/corerp/setup/operations.go @@ -0,0 +1,262 @@ +/* +Copyright 2023 The Radius Authors. + +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 setup + +import v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + +var operationList = []v1.Operation{ + { + Name: "Applications.Core/operations/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "operations", + Operation: "Get operations", + Description: "Get the list of operations", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/environments/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "environments", + Operation: "List environments", + Description: "Get the list of environments.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/environments/write", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "environments", + Operation: "Create/Update environment", + Description: "Create or update an environment.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/environments/delete", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "environments", + Operation: "Delete environment", + Description: "Delete an environment.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/environments/getmetadata/action", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "environments", + Operation: "Get recipe metadata", + Description: "Get recipe metadata.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/environments/join/action", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "environments", + Operation: "Join environment", + Description: "Join to application environment.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/register/action", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "Applications.Core", + Operation: "Register Applications.Core", + Description: "Register the subscription for Applications.Core.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/unregister/action", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "Applications.Core", + Operation: "Unregister Applications.Core", + Description: "Unregister the subscription for Applications.Core.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/httproutes/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "httproutes", + Operation: "List httproutes", + Description: "Get the list of httproutes.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/httproutes/write", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "httproutes", + Operation: "Create/Update httproute", + Description: "Create or update an httproute.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/httproutes/delete", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "httproutes", + Operation: "Delete httproute", + Description: "Delete an httproute.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/applications/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "applications", + Operation: "List applications", + Description: "Get the list of applications.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/applications/write", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "applications", + Operation: "Create/Update application", + Description: "Create or update an application.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/applications/delete", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "applications", + Operation: "Delete application", + Description: "Delete an application.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/gateways/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "gateways", + Operation: "List gateways", + Description: "Get the list of gateways.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/gateways/write", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "gateways", + Operation: "Create/Update gateway", + Description: "Create or Update a gateway.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/gateways/delete", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "gateways", + Operation: "delete gateway", + Description: "Delete a gateway.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/containers/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "containers", + Operation: "List containers", + Description: "Get the list of containers.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/containers/write", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "containers", + Operation: "Create/Update container", + Description: "Create or update a container.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/containers/delete", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "containers", + Operation: "Delete container", + Description: "Delete a container.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/extenders/read", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "extenders", + Operation: "Get/List extenders", + Description: "Gets/Lists extender link(s).", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/extenders/write", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "extenders", + Operation: "Create/Update extenders", + Description: "Creates or updates a extender resource.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/extenders/delete", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "extenders", + Operation: "Delete extender", + Description: "Deletes a extender resource.", + }, + IsDataAction: false, + }, + { + Name: "Applications.Core/extenders/listsecrets/action", + Display: &v1.OperationDisplayProperties{ + Provider: "Applications.Core", + Resource: "extenders", + Operation: "List secrets", + Description: "Lists extender secrets.", + }, + IsDataAction: false, + }, +} diff --git a/pkg/corerp/setup/setup.go b/pkg/corerp/setup/setup.go new file mode 100644 index 0000000000..b4884fa5ad --- /dev/null +++ b/pkg/corerp/setup/setup.go @@ -0,0 +1,217 @@ +/* +Copyright 2023 The Radius Authors. + +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 setup + +import ( + asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/armrpc/builder" + apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/corerp/datamodel" + "github.com/radius-project/radius/pkg/corerp/datamodel/converter" + "github.com/radius-project/radius/pkg/recipes/controllerconfig" + + backend_ctrl "github.com/radius-project/radius/pkg/corerp/backend/controller" + app_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/applications" + ctr_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/containers" + env_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/environments" + ext_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/extenders" + gw_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/gateways" + secret_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/secretstores" + vol_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/volumes" + rp_frontend "github.com/radius-project/radius/pkg/rp/frontend" + + ext_processor "github.com/radius-project/radius/pkg/corerp/processors/extenders" + pr_ctrl "github.com/radius-project/radius/pkg/portableresources/backend/controller" +) + +// SetupNamespace builds the namespace for core resource provider. +func SetupNamespace(recipeControllerConfig *controllerconfig.RecipeControllerConfig) *builder.Namespace { + ns := builder.NewNamespace("Applications.Core") + + _ = ns.AddResource("environments", &builder.ResourceOption[*datamodel.Environment, datamodel.Environment]{ + RequestConverter: converter.EnvironmentDataModelFromVersioned, + ResponseConverter: converter.EnvironmentDataModelToVersioned, + + Put: builder.Operation[datamodel.Environment]{ + APIController: env_ctrl.NewCreateOrUpdateEnvironment, + }, + Patch: builder.Operation[datamodel.Environment]{ + APIController: env_ctrl.NewCreateOrUpdateEnvironment, + }, + Custom: map[string]builder.Operation[datamodel.Environment]{ + "getmetadata": { + APIController: func(opt apictrl.Options) (apictrl.Controller, error) { + return env_ctrl.NewGetRecipeMetadata(opt, recipeControllerConfig.Engine) + }, + }, + }, + }) + + _ = ns.AddResource("applications", &builder.ResourceOption[*datamodel.Application, datamodel.Application]{ + RequestConverter: converter.ApplicationDataModelFromVersioned, + ResponseConverter: converter.ApplicationDataModelToVersioned, + + Put: builder.Operation[datamodel.Application]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.Application]{ + rp_frontend.PrepareRadiusResource[*datamodel.Application], + app_ctrl.CreateAppScopedNamespace, + }, + }, + Patch: builder.Operation[datamodel.Application]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.Application]{ + rp_frontend.PrepareRadiusResource[*datamodel.Application], + app_ctrl.CreateAppScopedNamespace, + }, + }, + }) + + _ = ns.AddResource("httpRoutes", &builder.ResourceOption[*datamodel.HTTPRoute, datamodel.HTTPRoute]{ + RequestConverter: converter.HTTPRouteDataModelFromVersioned, + ResponseConverter: converter.HTTPRouteDataModelToVersioned, + + Put: builder.Operation[datamodel.HTTPRoute]{ + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + Patch: builder.Operation[datamodel.HTTPRoute]{ + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + }) + + _ = ns.AddResource("containers", &builder.ResourceOption[*datamodel.ContainerResource, datamodel.ContainerResource]{ + RequestConverter: converter.ContainerDataModelFromVersioned, + ResponseConverter: converter.ContainerDataModelToVersioned, + + Put: builder.Operation[datamodel.ContainerResource]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.ContainerResource]{ + rp_frontend.PrepareRadiusResource[*datamodel.ContainerResource], + ctr_ctrl.ValidateAndMutateRequest, + }, + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + Patch: builder.Operation[datamodel.ContainerResource]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.ContainerResource]{ + rp_frontend.PrepareRadiusResource[*datamodel.ContainerResource], + ctr_ctrl.ValidateAndMutateRequest, + }, + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + }) + + _ = ns.AddResource("gateways", &builder.ResourceOption[*datamodel.Gateway, datamodel.Gateway]{ + RequestConverter: converter.GatewayDataModelFromVersioned, + ResponseConverter: converter.GatewayDataModelToVersioned, + + Put: builder.Operation[datamodel.Gateway]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.Gateway]{ + rp_frontend.PrepareRadiusResource[*datamodel.Gateway], + gw_ctrl.ValidateAndMutateRequest, + }, + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + Patch: builder.Operation[datamodel.Gateway]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.Gateway]{ + rp_frontend.PrepareRadiusResource[*datamodel.Gateway], + gw_ctrl.ValidateAndMutateRequest, + }, + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + }) + + _ = ns.AddResource("volumes", &builder.ResourceOption[*datamodel.VolumeResource, datamodel.VolumeResource]{ + RequestConverter: converter.VolumeResourceModelFromVersioned, + ResponseConverter: converter.VolumeResourceModelToVersioned, + + Put: builder.Operation[datamodel.VolumeResource]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.VolumeResource]{ + rp_frontend.PrepareRadiusResource[*datamodel.VolumeResource], + vol_ctrl.ValidateRequest, + }, + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + Patch: builder.Operation[datamodel.VolumeResource]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.VolumeResource]{ + rp_frontend.PrepareRadiusResource[*datamodel.VolumeResource], + vol_ctrl.ValidateRequest, + }, + AsyncJobController: backend_ctrl.NewCreateOrUpdateResource, + }, + }) + + _ = ns.AddResource("secretStores", &builder.ResourceOption[*datamodel.SecretStore, datamodel.SecretStore]{ + RequestConverter: converter.SecretStoreModelFromVersioned, + ResponseConverter: converter.SecretStoreModelToVersioned, + + Put: builder.Operation[datamodel.SecretStore]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.SecretStore]{ + rp_frontend.PrepareRadiusResource[*datamodel.SecretStore], + secret_ctrl.ValidateAndMutateRequest, + secret_ctrl.UpsertSecret, + }, + }, + Patch: builder.Operation[datamodel.SecretStore]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.SecretStore]{ + rp_frontend.PrepareRadiusResource[*datamodel.SecretStore], + secret_ctrl.ValidateAndMutateRequest, + secret_ctrl.UpsertSecret, + }, + }, + Delete: builder.Operation[datamodel.SecretStore]{ + DeleteFilters: []apictrl.DeleteFilter[datamodel.SecretStore]{ + secret_ctrl.DeleteRadiusSecret, + }, + }, + Custom: map[string]builder.Operation[datamodel.SecretStore]{ + "listsecrets": { + APIController: secret_ctrl.NewListSecrets, + }, + }, + }) + + _ = ns.AddResource("extenders", &builder.ResourceOption[*datamodel.Extender, datamodel.Extender]{ + RequestConverter: converter.ExtenderDataModelFromVersioned, + ResponseConverter: converter.ExtenderDataModelToVersioned, + + Put: builder.Operation[datamodel.Extender]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.Extender]{ + rp_frontend.PrepareRadiusResource[*datamodel.Extender], + }, + AsyncJobController: func(options asyncctrl.Options) (asyncctrl.Controller, error) { + return pr_ctrl.NewCreateOrUpdateResource(options, &ext_processor.Processor{}, recipeControllerConfig.Engine, recipeControllerConfig.ResourceClient, recipeControllerConfig.ConfigLoader) + }, + }, + Patch: builder.Operation[datamodel.Extender]{ + UpdateFilters: []apictrl.UpdateFilter[datamodel.Extender]{ + rp_frontend.PrepareRadiusResource[*datamodel.Extender], + }, + }, + Delete: builder.Operation[datamodel.Extender]{ + AsyncJobController: func(options asyncctrl.Options) (asyncctrl.Controller, error) { + return pr_ctrl.NewDeleteResource(options, &ext_processor.Processor{}, recipeControllerConfig.Engine, recipeControllerConfig.ConfigLoader) + }, + }, + Custom: map[string]builder.Operation[datamodel.Extender]{ + "listsecrets": { + APIController: ext_ctrl.NewListSecretsExtender, + }, + }, + }) + + // Optional + ns.SetAvailableOperations(operationList) + + return ns +} diff --git a/pkg/corerp/setup/setup_test.go b/pkg/corerp/setup/setup_test.go new file mode 100644 index 0000000000..f6817359e0 --- /dev/null +++ b/pkg/corerp/setup/setup_test.go @@ -0,0 +1,253 @@ +/* +Copyright 2023 The Radius Authors. + +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 setup + +import ( + "context" + "net/http" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/armrpc/builder" + apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/radius-project/radius/pkg/recipes/controllerconfig" + "github.com/radius-project/radius/pkg/ucp/dataprovider" + "github.com/radius-project/radius/pkg/ucp/store" + + app_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/applications" + ctr_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/containers" + env_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/environments" + gtwy_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/gateways" + hrt_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/httproutes" + secret_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/secretstores" + vol_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/volumes" +) + +var handlerTests = []rpctest.HandlerTestSpec{ + { + OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.core/applications", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.core/applications", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.core/applications/app0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.core/applications/app0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.core/applications/app0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.core/applications/app0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.core/containers", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.core/containers", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.core/environments", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.core/environments", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.core/environments/env0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.core/environments/env0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.core/environments/env0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.core/environments/env0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: "ACTIONGETMETADATA"}, + Path: "/resourcegroups/testrg/providers/applications.core/environments/env0/getmetadata", + Method: http.MethodPost, + }, { + OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.core/gateways", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.core/gateways", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.core/httproutes", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.core/httproutes", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.core/secretstores", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.core/secretstores", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: "ACTIONLISTSECRETS"}, + Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0/listsecrets", + Method: http.MethodPost, + }, { + OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList}, + Path: "/providers/applications.core/volumes", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationList}, + Path: "/resourcegroups/testrg/providers/applications.core/volumes", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationGet}, + Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationPut}, + Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0", + Method: http.MethodPut, + }, { + OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationPatch}, + Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0", + Method: http.MethodPatch, + }, { + OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationDelete}, + Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0", + Method: http.MethodDelete, + }, { + OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGet}, + Path: "/providers/applications.core/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000", + Method: http.MethodGet, + }, { + OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGet}, + Path: "/providers/applications.core/locations/global/operationresults/00000000-0000-0000-0000-000000000000", + Method: http.MethodGet, + }, +} + +func TestRouter(t *testing.T) { + mctrl := gomock.NewController(t) + + mockSP := dataprovider.NewMockDataStorageProvider(mctrl) + mockSC := store.NewMockStorageClient(mctrl) + + mockSC.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&store.Object{}, nil).AnyTimes() + mockSC.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + mockSP.EXPECT().GetStorageClient(gomock.Any(), gomock.Any()).Return(store.StorageClient(mockSC), nil).AnyTimes() + + cfg := &controllerconfig.RecipeControllerConfig{} + ns := SetupNamespace(cfg) + nsBuilder := ns.GenerateBuilder() + + rpctest.AssertRouters(t, handlerTests, "/api.ucp.dev", "/planes/radius/local", func(ctx context.Context) (chi.Router, error) { + r := chi.NewRouter() + validator, err := builder.NewOpenAPIValidator(ctx, "/api.ucp.dev", "applications.core") + require.NoError(t, err) + return r, nsBuilder.ApplyAPIHandlers(ctx, r, apictrl.Options{PathBase: "/api.ucp.dev", DataProvider: mockSP}, validator) + }) +} diff --git a/pkg/kubeutil/client.go b/pkg/kubeutil/client.go index 9b83219ff3..593a9269c5 100644 --- a/pkg/kubeutil/client.go +++ b/pkg/kubeutil/client.go @@ -21,12 +21,62 @@ import ( apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" csidriver "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" ) +// Clients is a collection of Kubernetes clients. +type Clients struct { + // RuntimeClient is the Kubernetes controller runtime client. + RuntimeClient runtimeclient.Client + + // ClientSet is the Kubernetes client-go strongly-typed client. + ClientSet *kubernetes.Clientset + + // DiscoveryClient is the Kubernetes client-go discovery client. + DiscoveryClient *discovery.DiscoveryClient + + // DynamicClient is the Kubernetes client-go dynamic client. + DynamicClient dynamic.Interface +} + +// NewClients creates a new Kubernetes client set and controller runtime client using the given config. +func NewClients(config *rest.Config) (*Clients, error) { + c := &Clients{} + + var err error + c.RuntimeClient, err = NewRuntimeClient(config) + if err != nil { + return nil, err + } + + c.ClientSet, err = kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + c.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return nil, err + } + + // Use legacy discovery client to avoid the issue of the staled GroupVersion discovery(api.ucp.dev/v1alpha3). + // TODO: Disable UseLegacyDiscovery once https://github.com/radius-project/radius/issues/5974 is resolved. + c.DiscoveryClient.UseLegacyDiscovery = true + + c.DynamicClient, err = dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + + return c, nil +} + // NewRuntimeClient creates a new runtime client using the given config and adds the // required resource schemes to the client. func NewRuntimeClient(config *rest.Config) (runtimeclient.Client, error) { diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index 427384ab29..ac5e5a9f87 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -24,20 +24,16 @@ import ( ) // WithLogger adds logger to the context based on the Resource ID (if present). -func WithLogger(serviceName string) func(h http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - id, err := resources.Parse(r.URL.Path) - if err != nil { - // This just means the request is for an ARM resource. Not an error. - h.ServeHTTP(w, r) - return - } - - ctx := ucplog.WrapLogContext(r.Context(), ucplog.LogFieldResourceID, id.String()) - h.ServeHTTP(w, r.WithContext(ctx)) +func WithLogger(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id, err := resources.Parse(r.URL.Path) + if err != nil { + // This just means the request is for an ARM resource. Not an error. + h.ServeHTTP(w, r) + return } - return http.HandlerFunc(fn) - } + ctx := ucplog.WrapLogContext(r.Context(), ucplog.LogFieldResourceID, id.String()) + h.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/pkg/portableresources/backend/controller/createorupdateresource.go b/pkg/portableresources/backend/controller/createorupdateresource.go index d41de121d2..05aad4e8a4 100644 --- a/pkg/portableresources/backend/controller/createorupdateresource.go +++ b/pkg/portableresources/backend/controller/createorupdateresource.go @@ -47,7 +47,7 @@ type CreateOrUpdateResource[P interface { func NewCreateOrUpdateResource[P interface { *T rpv1.RadiusResourceModel -}, T any](processor processors.ResourceProcessor[P, T], eng engine.Engine, client processors.ResourceClient, configurationLoader configloader.ConfigurationLoader, opts ctrl.Options) (ctrl.Controller, error) { +}, T any](opts ctrl.Options, processor processors.ResourceProcessor[P, T], eng engine.Engine, client processors.ResourceClient, configurationLoader configloader.ConfigurationLoader) (ctrl.Controller, error) { return &CreateOrUpdateResource[P, T]{ ctrl.NewBaseAsyncController(opts), processor, diff --git a/pkg/portableresources/backend/controller/createorupdateresource_test.go b/pkg/portableresources/backend/controller/createorupdateresource_test.go index 6a15df3873..ea65a55a08 100644 --- a/pkg/portableresources/backend/controller/createorupdateresource_test.go +++ b/pkg/portableresources/backend/controller/createorupdateresource_test.go @@ -33,6 +33,7 @@ import ( "github.com/radius-project/radius/pkg/portableresources/processors" "github.com/radius-project/radius/pkg/recipes" "github.com/radius-project/radius/pkg/recipes/configloader" + "github.com/radius-project/radius/pkg/recipes/controllerconfig" "github.com/radius-project/radius/pkg/recipes/engine" rpv1 "github.com/radius-project/radius/pkg/rp/v1" "github.com/radius-project/radius/pkg/ucp/resources" @@ -139,7 +140,7 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { cases := []struct { description string - factory func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) + factory func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) getErr error conversionFailure bool recipeErr error @@ -151,8 +152,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }{ { "get-not-found", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, &store.ErrNotFound{ID: TestResourceID}, false, @@ -165,8 +166,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }, { "get-error", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, &store.ErrInvalid{}, false, @@ -179,8 +180,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }, { "conversion-failure", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, nil, true, @@ -193,8 +194,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }, { "recipe-err", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, nil, false, @@ -207,8 +208,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }, { "runtime-configuration-err", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, nil, false, @@ -221,8 +222,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }, { "processor-err", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, nil, false, @@ -235,8 +236,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }, { "save-err", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(successProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, successProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, nil, false, @@ -249,8 +250,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { }, { "success", - func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) { - return NewCreateOrUpdateResource(successProcessorReference, eng, client, cfg, options) + func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) { + return NewCreateOrUpdateResource(options, successProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader) }, nil, false, @@ -403,7 +404,13 @@ func TestCreateOrUpdateResource_Run(t *testing.T) { StorageClient: msc, } - genCtrl, err := tt.factory(eng, client, cfg, opts) + recipeCfg := &controllerconfig.RecipeControllerConfig{ + Engine: eng, + ResourceClient: client, + ConfigLoader: cfg, + } + + genCtrl, err := tt.factory(recipeCfg, opts) require.NoError(t, err) res, err := genCtrl.Run(context.Background(), req) diff --git a/pkg/portableresources/backend/controller/deleteresource.go b/pkg/portableresources/backend/controller/deleteresource.go index d33ed3ea4f..67d374b25a 100644 --- a/pkg/portableresources/backend/controller/deleteresource.go +++ b/pkg/portableresources/backend/controller/deleteresource.go @@ -45,7 +45,7 @@ type DeleteResource[P interface { func NewDeleteResource[P interface { *T rpv1.RadiusResourceModel -}, T any](processor processors.ResourceProcessor[P, T], eng engine.Engine, configurationLoader configloader.ConfigurationLoader, opts ctrl.Options) (ctrl.Controller, error) { +}, T any](opts ctrl.Options, processor processors.ResourceProcessor[P, T], eng engine.Engine, configurationLoader configloader.ConfigurationLoader) (ctrl.Controller, error) { return &DeleteResource[P, T]{ ctrl.NewBaseAsyncController(opts), processor, diff --git a/pkg/portableresources/backend/controller/deleteresource_test.go b/pkg/portableresources/backend/controller/deleteresource_test.go index e101f56525..2fc612b3da 100644 --- a/pkg/portableresources/backend/controller/deleteresource_test.go +++ b/pkg/portableresources/backend/controller/deleteresource_test.go @@ -151,7 +151,7 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) { StorageClient: msc, } - ctrl, err := NewDeleteResource(successProcessorReference, eng, configLoader, opts) + ctrl, err := NewDeleteResource(opts, successProcessorReference, eng, configLoader) require.NoError(t, err) _, err = ctrl.Run(context.Background(), req) diff --git a/pkg/portableresources/backend/service.go b/pkg/portableresources/backend/service.go index 440765cc94..808fed7e60 100644 --- a/pkg/portableresources/backend/service.go +++ b/pkg/portableresources/backend/service.go @@ -23,7 +23,6 @@ import ( v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker" "github.com/radius-project/radius/pkg/armrpc/hostoptions" - aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" dapr_dm "github.com/radius-project/radius/pkg/daprrp/datamodel" "github.com/radius-project/radius/pkg/daprrp/processors/pubsubbrokers" "github.com/radius-project/radius/pkg/daprrp/processors/secretstores" @@ -32,23 +31,16 @@ import ( mongo_prc "github.com/radius-project/radius/pkg/datastoresrp/processors/mongodatabases" redis_prc "github.com/radius-project/radius/pkg/datastoresrp/processors/rediscaches" sql_prc "github.com/radius-project/radius/pkg/datastoresrp/processors/sqldatabases" + "github.com/radius-project/radius/pkg/kubeutil" msg_dm "github.com/radius-project/radius/pkg/messagingrp/datamodel" "github.com/radius-project/radius/pkg/messagingrp/processors/rabbitmqqueues" "github.com/radius-project/radius/pkg/portableresources" "github.com/radius-project/radius/pkg/portableresources/frontend/handler" - "github.com/radius-project/radius/pkg/portableresources/processors" - "github.com/radius-project/radius/pkg/recipes" - "github.com/radius-project/radius/pkg/recipes/configloader" - "github.com/radius-project/radius/pkg/recipes/driver" - "github.com/radius-project/radius/pkg/recipes/engine" - "github.com/radius-project/radius/pkg/sdk" - "github.com/radius-project/radius/pkg/sdk/clients" + "github.com/radius-project/radius/pkg/recipes/controllerconfig" ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" backend_ctrl "github.com/radius-project/radius/pkg/portableresources/backend/controller" - - "github.com/radius-project/radius/pkg/ucp/secret/provider" ) type Service struct { @@ -76,29 +68,19 @@ func (s *Service) Run(ctx context.Context) error { return err } - client := processors.NewResourceClient(s.Options.Arm, s.Options.UCPConnection, s.KubeClient, s.KubeDiscoveryClient) - clientOptions := sdk.NewClientOptions(s.Options.UCPConnection) + k8s, err := kubeutil.NewClients(s.Options.K8sConfig) + if err != nil { + return fmt.Errorf("failed to initialize kubernetes client: %w", err) + } - deploymentEngineClient, err := clients.NewResourceDeploymentsClient(&clients.Options{ - Cred: &aztoken.AnonymousCredential{}, - BaseURI: s.Options.UCPConnection.Endpoint(), - ARMClientOptions: clientOptions, - }) + recipeControllerConfig, err := controllerconfig.New(s.Options) if err != nil { return err } - configLoader := configloader.NewEnvironmentLoader(clientOptions) - engine := engine.NewEngine(engine.Options{ - ConfigurationLoader: configLoader, - Drivers: map[string]driver.Driver{ - recipes.TemplateKindBicep: driver.NewBicepDriver(clientOptions, deploymentEngineClient, client), - recipes.TemplateKindTerraform: driver.NewTerraformDriver(s.Options.UCPConnection, provider.NewSecretProvider(s.Options.Config.SecretProvider), - driver.TerraformOptions{ - Path: s.Options.Config.Terraform.Path, - }, s.KubeClientSet), - }, - }) + engine := recipeControllerConfig.Engine + client := recipeControllerConfig.ResourceClient + configLoader := recipeControllerConfig.ConfigLoader // resourceTypes is the array that holds resource types that needs async processing. // We use this array to register backend controllers for each resource. @@ -111,84 +93,84 @@ func (s *Service) Run(ctx context.Context) error { portableresources.RabbitMQQueuesResourceType, func(options ctrl.Options) (ctrl.Controller, error) { processor := &rabbitmqqueues.Processor{} - return backend_ctrl.NewCreateOrUpdateResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](processor, engine, client, configLoader, options) + return backend_ctrl.NewCreateOrUpdateResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](options, processor, engine, client, configLoader) }, func(options ctrl.Options) (ctrl.Controller, error) { processor := &rabbitmqqueues.Processor{} - return backend_ctrl.NewDeleteResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](processor, engine, configLoader, options) + return backend_ctrl.NewDeleteResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](options, processor, engine, configLoader) }, }, { portableresources.DaprStateStoresResourceType, func(options ctrl.Options) (ctrl.Controller, error) { - processor := &statestores.Processor{Client: s.KubeClient} - return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](processor, engine, client, configLoader, options) + processor := &statestores.Processor{Client: k8s.RuntimeClient} + return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](options, processor, engine, client, configLoader) }, func(options ctrl.Options) (ctrl.Controller, error) { - processor := &statestores.Processor{Client: s.KubeClient} - return backend_ctrl.NewDeleteResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](processor, engine, configLoader, options) + processor := &statestores.Processor{Client: k8s.RuntimeClient} + return backend_ctrl.NewDeleteResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](options, processor, engine, configLoader) }, }, { portableresources.DaprSecretStoresResourceType, func(options ctrl.Options) (ctrl.Controller, error) { - processor := &secretstores.Processor{Client: s.KubeClient} - return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](processor, engine, client, configLoader, options) + processor := &secretstores.Processor{Client: k8s.RuntimeClient} + return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](options, processor, engine, client, configLoader) }, func(options ctrl.Options) (ctrl.Controller, error) { - processor := &secretstores.Processor{Client: s.KubeClient} - return backend_ctrl.NewDeleteResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](processor, engine, configLoader, options) + processor := &secretstores.Processor{Client: k8s.RuntimeClient} + return backend_ctrl.NewDeleteResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](options, processor, engine, configLoader) }, }, { portableresources.DaprPubSubBrokersResourceType, func(options ctrl.Options) (ctrl.Controller, error) { - processor := &pubsubbrokers.Processor{Client: s.KubeClient} - return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](processor, engine, client, configLoader, options) + processor := &pubsubbrokers.Processor{Client: k8s.RuntimeClient} + return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](options, processor, engine, client, configLoader) }, func(options ctrl.Options) (ctrl.Controller, error) { - processor := &pubsubbrokers.Processor{Client: s.KubeClient} - return backend_ctrl.NewDeleteResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](processor, engine, configLoader, options) + processor := &pubsubbrokers.Processor{Client: k8s.RuntimeClient} + return backend_ctrl.NewDeleteResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](options, processor, engine, configLoader) }, }, { portableresources.MongoDatabasesResourceType, func(options ctrl.Options) (ctrl.Controller, error) { processor := &mongo_prc.Processor{} - return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](processor, engine, client, configLoader, options) + return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](options, processor, engine, client, configLoader) }, func(options ctrl.Options) (ctrl.Controller, error) { processor := &mongo_prc.Processor{} - return backend_ctrl.NewDeleteResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](processor, engine, configLoader, options) + return backend_ctrl.NewDeleteResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](options, processor, engine, configLoader) }, }, { portableresources.RedisCachesResourceType, func(options ctrl.Options) (ctrl.Controller, error) { processor := &redis_prc.Processor{} - return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.RedisCache, ds_dm.RedisCache](processor, engine, client, configLoader, options) + return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.RedisCache, ds_dm.RedisCache](options, processor, engine, client, configLoader) }, func(options ctrl.Options) (ctrl.Controller, error) { processor := &redis_prc.Processor{} - return backend_ctrl.NewDeleteResource[*ds_dm.RedisCache, ds_dm.RedisCache](processor, engine, configLoader, options) + return backend_ctrl.NewDeleteResource[*ds_dm.RedisCache, ds_dm.RedisCache](options, processor, engine, configLoader) }, }, { portableresources.SqlDatabasesResourceType, func(options ctrl.Options) (ctrl.Controller, error) { processor := &sql_prc.Processor{} - return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](processor, engine, client, configLoader, options) + return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](options, processor, engine, client, configLoader) }, func(options ctrl.Options) (ctrl.Controller, error) { processor := &sql_prc.Processor{} - return backend_ctrl.NewDeleteResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](processor, engine, configLoader, options) + return backend_ctrl.NewDeleteResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](options, processor, engine, configLoader) }, }, } opts := ctrl.Options{ DataProvider: s.StorageProvider, - KubeClient: s.KubeClient, + KubeClient: k8s.RuntimeClient, } for _, rt := range resourceTypes { @@ -197,13 +179,11 @@ func (s *Service) Run(ctx context.Context) error { if err != nil { return err } - err = s.Controllers.Register(ctx, rt.TypeName, v1.OperationPut, rt.CreatePutController, opts) if err != nil { return err } } - workerOpts := worker.Options{} if s.Options.Config.WorkerServer != nil { if s.Options.Config.WorkerServer.MaxOperationConcurrency != nil { diff --git a/pkg/portableresources/frontend/handler/routes_test.go b/pkg/portableresources/frontend/handler/routes_test.go index 29802a4bf4..ccc8f96a65 100644 --- a/pkg/portableresources/frontend/handler/routes_test.go +++ b/pkg/portableresources/frontend/handler/routes_test.go @@ -219,20 +219,20 @@ var handlerTests = []rpctest.HandlerTestSpec{ Path: "/resourcegroups/testrg/providers/applications.datastores/sqldatabases/sql/listsecrets", Method: http.MethodPost, }, { - OperationType: v1.OperationType{Type: "Applications.Messaging/operationStatuses", Method: v1.OperationGetOperationStatuses}, + OperationType: v1.OperationType{Type: "Applications.Messaging/operationStatuses", Method: v1.OperationGet}, Path: "/providers/applications.messaging/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000", Method: http.MethodGet, }, { - OperationType: v1.OperationType{Type: "Applications.Messaging/operationStatuses", Method: v1.OperationGetOperationResult}, + OperationType: v1.OperationType{Type: "Applications.Messaging/operationResults", Method: v1.OperationGet}, Path: "/providers/applications.messaging/locations/global/operationresults/00000000-0000-0000-0000-000000000000", Method: http.MethodGet, }, { - OperationType: v1.OperationType{Type: "Applications.Dapr/operationStatuses", Method: v1.OperationGetOperationStatuses}, + OperationType: v1.OperationType{Type: "Applications.Dapr/operationStatuses", Method: v1.OperationGet}, Path: "/providers/applications.dapr/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000", Method: http.MethodGet, }, { - OperationType: v1.OperationType{Type: "Applications.Dapr/operationStatuses", Method: v1.OperationGetOperationResult}, + OperationType: v1.OperationType{Type: "Applications.Dapr/operationResults", Method: v1.OperationGet}, Path: "/providers/applications.dapr/locations/global/operationresults/00000000-0000-0000-0000-000000000000", Method: http.MethodGet, }, diff --git a/pkg/portableresources/frontend/service.go b/pkg/portableresources/frontend/service.go index b83a0f4027..eb35116912 100644 --- a/pkg/portableresources/frontend/service.go +++ b/pkg/portableresources/frontend/service.go @@ -61,10 +61,10 @@ func (s *Service) Run(ctx context.Context) error { } err := s.Start(ctx, server.Options{ - Address: opts.Address, - ProviderNamespace: s.ProviderName, - Location: s.Options.Config.Env.RoleLocation, - PathBase: s.Options.Config.Server.PathBase, + Address: opts.Address, + ServiceName: s.ProviderName, + Location: s.Options.Config.Env.RoleLocation, + PathBase: s.Options.Config.Server.PathBase, // set the arm cert manager for managing client certificate ArmCertMgr: s.ARMCertManager, EnableArmAuth: s.Options.Config.Server.EnableArmAuth, // when enabled the client cert validation will be done diff --git a/pkg/recipes/controllerconfig/config.go b/pkg/recipes/controllerconfig/config.go new file mode 100644 index 0000000000..e40b93cf87 --- /dev/null +++ b/pkg/recipes/controllerconfig/config.go @@ -0,0 +1,85 @@ +/* +Copyright 2023 The Radius Authors. + +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 controllerconfig + +import ( + "github.com/radius-project/radius/pkg/armrpc/hostoptions" + aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" + "github.com/radius-project/radius/pkg/kubeutil" + "github.com/radius-project/radius/pkg/portableresources/processors" + "github.com/radius-project/radius/pkg/recipes" + "github.com/radius-project/radius/pkg/recipes/configloader" + "github.com/radius-project/radius/pkg/recipes/driver" + "github.com/radius-project/radius/pkg/recipes/engine" + "github.com/radius-project/radius/pkg/sdk" + "github.com/radius-project/radius/pkg/sdk/clients" + "github.com/radius-project/radius/pkg/ucp/secret/provider" +) + +// RecipeControllerConfig is the configuration for the controllers which uses recipe. +type RecipeControllerConfig struct { + // K8sClients is the collections of Kubernetes clients. + K8sClients *kubeutil.Clients + + // ResourceClient is a client used by resource processors for interacting with UCP resources. + ResourceClient processors.ResourceClient + + // ConfigLoader is the configuration loader. + ConfigLoader configloader.ConfigurationLoader + + // DeploymentEngineClient is the client for interacting with the deployment engine. + DeploymentEngineClient *clients.ResourceDeploymentsClient + + // Engine is the engine for executing recipes. + Engine engine.Engine +} + +// New creates a new RecipeControllerConfig instance with the given host options. +func New(options hostoptions.HostOptions) (*RecipeControllerConfig, error) { + cfg := &RecipeControllerConfig{} + var err error + cfg.K8sClients, err = kubeutil.NewClients(options.K8sConfig) + if err != nil { + return nil, err + } + + cfg.ResourceClient = processors.NewResourceClient(options.Arm, options.UCPConnection, cfg.K8sClients.RuntimeClient, cfg.K8sClients.DiscoveryClient) + clientOptions := sdk.NewClientOptions(options.UCPConnection) + + cfg.DeploymentEngineClient, err = clients.NewResourceDeploymentsClient(&clients.Options{ + Cred: &aztoken.AnonymousCredential{}, + BaseURI: options.UCPConnection.Endpoint(), + ARMClientOptions: sdk.NewClientOptions(options.UCPConnection), + }) + if err != nil { + return nil, err + } + + cfg.ConfigLoader = configloader.NewEnvironmentLoader(clientOptions) + cfg.Engine = engine.NewEngine(engine.Options{ + ConfigurationLoader: cfg.ConfigLoader, + Drivers: map[string]driver.Driver{ + recipes.TemplateKindBicep: driver.NewBicepDriver(clientOptions, cfg.DeploymentEngineClient, cfg.ResourceClient), + recipes.TemplateKindTerraform: driver.NewTerraformDriver(options.UCPConnection, provider.NewSecretProvider(options.Config.SecretProvider), + driver.TerraformOptions{ + Path: options.Config.Terraform.Path, + }, cfg.K8sClients.ClientSet), + }, + }) + + return cfg, nil +} diff --git a/pkg/server/apiservice.go b/pkg/server/apiservice.go new file mode 100644 index 0000000000..b92d6fe2f2 --- /dev/null +++ b/pkg/server/apiservice.go @@ -0,0 +1,89 @@ +/* +Copyright 2023 The Radius Authors. + +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 server + +import ( + "context" + "fmt" + + "github.com/go-chi/chi/v5" + + "github.com/radius-project/radius/pkg/armrpc/builder" + apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller" + "github.com/radius-project/radius/pkg/armrpc/frontend/server" + "github.com/radius-project/radius/pkg/armrpc/hostoptions" +) + +// APIService is the restful API server for Radius Resource Provider. +type APIService struct { + server.Service + + handlerBuilder []builder.Builder +} + +// NewAPIService creates a new instance of APIService. +func NewAPIService(options hostoptions.HostOptions, builder []builder.Builder) *APIService { + return &APIService{ + Service: server.Service{ + ProviderName: "radius", + Options: options, + }, + handlerBuilder: builder, + } +} + +// Name returns the name of the service. +func (s *APIService) Name() string { + return "radiusapi" +} + +// Run starts the service. +func (s *APIService) Run(ctx context.Context) error { + if err := s.Init(ctx); err != nil { + return err + } + + address := fmt.Sprintf("%s:%d", s.Options.Config.Server.Host, s.Options.Config.Server.Port) + return s.Start(ctx, server.Options{ + Location: s.Options.Config.Env.RoleLocation, + Address: address, + PathBase: s.Options.Config.Server.PathBase, + Configure: func(r chi.Router) error { + for _, b := range s.handlerBuilder { + opts := apictrl.Options{ + PathBase: s.Options.Config.Server.PathBase, + DataProvider: s.StorageProvider, + KubeClient: s.KubeClient, + StatusManager: s.OperationStatusManager, + } + + validator, err := builder.NewOpenAPIValidator(ctx, opts.PathBase, b.Namespace()) + if err != nil { + panic(err) + } + + if err := b.ApplyAPIHandlers(ctx, r, opts, validator); err != nil { + panic(err) + } + } + return nil + }, + // set the arm cert manager for managing client certificate + ArmCertMgr: s.ARMCertManager, + EnableArmAuth: s.Options.Config.Server.EnableArmAuth, // when enabled the client cert validation will be done + }) +} diff --git a/pkg/server/asyncworker.go b/pkg/server/asyncworker.go new file mode 100644 index 0000000000..15dfb3850d --- /dev/null +++ b/pkg/server/asyncworker.go @@ -0,0 +1,97 @@ +/* +Copyright 2023 The Radius Authors. + +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 server + +import ( + "context" + "fmt" + + ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker" + "github.com/radius-project/radius/pkg/armrpc/builder" + "github.com/radius-project/radius/pkg/armrpc/hostoptions" + "github.com/radius-project/radius/pkg/corerp/backend/deployment" + "github.com/radius-project/radius/pkg/corerp/model" + "github.com/radius-project/radius/pkg/kubeutil" +) + +// AsyncWorker is a service to run AsyncReqeustProcessWorker. +type AsyncWorker struct { + worker.Service + + handlerBuilder []builder.Builder +} + +// NewAsyncWorker creates new service instance to run AsyncReqeustProcessWorker. +func NewAsyncWorker(options hostoptions.HostOptions, builder []builder.Builder) *AsyncWorker { + return &AsyncWorker{ + Service: worker.Service{ + ProviderName: "radius", + Options: options, + }, + handlerBuilder: builder, + } +} + +// Name represents the service name. +func (w *AsyncWorker) Name() string { + return "radiusasyncworker" +} + +// Run starts the service and worker. +func (w *AsyncWorker) Run(ctx context.Context) error { + if err := w.Init(ctx); err != nil { + return err + } + + k8s, err := kubeutil.NewClients(w.Options.K8sConfig) + if err != nil { + return fmt.Errorf("failed to initialize kubernetes clients: %w", err) + } + + appModel, err := model.NewApplicationModel(w.Options.Arm, k8s.RuntimeClient, k8s.ClientSet, k8s.DiscoveryClient, k8s.DynamicClient) + if err != nil { + return fmt.Errorf("failed to initialize application model: %w", err) + } + + for _, b := range w.handlerBuilder { + opts := ctrl.Options{ + DataProvider: w.StorageProvider, + KubeClient: k8s.RuntimeClient, + GetDeploymentProcessor: func() deployment.DeploymentProcessor { + return deployment.NewDeploymentProcessor(appModel, w.StorageProvider, k8s.RuntimeClient, k8s.ClientSet) + }, + } + + err := b.ApplyAsyncHandler(ctx, w.Controllers, opts) + if err != nil { + panic(err) + } + } + + workerOpts := worker.Options{} + if w.Options.Config.WorkerServer != nil { + if w.Options.Config.WorkerServer.MaxOperationConcurrency != nil { + workerOpts.MaxOperationConcurrency = *w.Options.Config.WorkerServer.MaxOperationConcurrency + } + if w.Options.Config.WorkerServer.MaxOperationRetryCount != nil { + workerOpts.MaxOperationRetryCount = *w.Options.Config.WorkerServer.MaxOperationRetryCount + } + } + + return w.Start(ctx, workerOpts) +} diff --git a/pkg/ucp/frontend/api/server.go b/pkg/ucp/frontend/api/server.go index 7c3fa20045..78f88b04a6 100644 --- a/pkg/ucp/frontend/api/server.go +++ b/pkg/ucp/frontend/api/server.go @@ -117,7 +117,7 @@ func (s *Service) Initialize(ctx context.Context) (*http.Server, error) { r := chi.NewRouter() s.storageProvider = dataprovider.NewStorageProvider(s.options.StorageProviderOptions) - s.queueProvider = queueprovider.New(s.options.ProviderName, s.options.QueueProviderOptions) + s.queueProvider = queueprovider.New(s.options.QueueProviderOptions) s.secretProvider = secretprovider.NewSecretProvider(s.options.SecretProviderOptions) specLoader, err := validator.LoadSpec(ctx, "ucp", swagger.SpecFilesUCP, []string{s.options.PathBase}, "") @@ -154,7 +154,7 @@ func (s *Service) Initialize(ctx context.Context) (*http.Server, error) { app := http.Handler(r) app = servicecontext.ARMRequestCtx(s.options.PathBase, "global")(app) - app = middleware.WithLogger("ucp")(app) + app = middleware.WithLogger(app) app = otelhttp.NewHandler( middleware.NormalizePath(app), diff --git a/pkg/ucp/frontend/aws/module.go b/pkg/ucp/frontend/aws/module.go index 49e5f31646..676b4d0966 100644 --- a/pkg/ucp/frontend/aws/module.go +++ b/pkg/ucp/frontend/aws/module.go @@ -23,6 +23,14 @@ import ( "github.com/radius-project/radius/pkg/validator" ) +const ( + // OperationTypeAWSResource is the operation status type for AWS resources. + OperationStatusResourceType = "System.AWS/operationStatuses" + + // OperationTypeAWSResource is the operation result type for AWS resources. + OperationResultsResourceType = "System.AWS/operationResults" +) + // NewModule creates a new AWS module. func NewModule(options modules.Options) *Module { m := Module{options: options} diff --git a/pkg/ucp/frontend/aws/routes.go b/pkg/ucp/frontend/aws/routes.go index 85cc8e609a..2c780c07b1 100644 --- a/pkg/ucp/frontend/aws/routes.go +++ b/pkg/ucp/frontend/aws/routes.go @@ -84,8 +84,8 @@ func (m *Module) Initialize(ctx context.Context) (http.Handler, error) { { // URLs for standard UCP resource async status result. ParentRouter: server.NewSubrouter(baseRouter, operationResultsPath), - Method: v1.OperationGetOperationResult, - OperationType: &v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationResult}, + Method: v1.OperationGet, + OperationType: &v1.OperationType{Type: OperationResultsResourceType, Method: v1.OperationGet}, ControllerFactory: func(opt controller.Options) (controller.Controller, error) { return awsproxy_ctrl.NewGetAWSOperationResults(opt, m.AWSClients) }, @@ -93,8 +93,8 @@ func (m *Module) Initialize(ctx context.Context) (http.Handler, error) { { // URLs for standard UCP resource async status. ParentRouter: server.NewSubrouter(baseRouter, operationStatusesPath), - Method: v1.OperationGetOperationStatuses, - OperationType: &v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationStatuses}, + Method: v1.OperationGet, + OperationType: &v1.OperationType{Type: OperationStatusResourceType, Method: v1.OperationGet}, ControllerFactory: func(opts controller.Options) (controller.Controller, error) { return awsproxy_ctrl.NewGetAWSOperationStatuses(opts, m.AWSClients) }, diff --git a/pkg/ucp/frontend/aws/routes_test.go b/pkg/ucp/frontend/aws/routes_test.go index 0549bc3387..7642d2cc3d 100644 --- a/pkg/ucp/frontend/aws/routes_test.go +++ b/pkg/ucp/frontend/aws/routes_test.go @@ -83,11 +83,11 @@ func Test_Routes(t *testing.T) { Method: http.MethodPost, Path: "/planes/aws/aws/accounts/0000000/regions/some-region/providers/AWS.Kinesis/Stream/:delete", }, { - OperationType: v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationResult}, + OperationType: v1.OperationType{Type: OperationResultsResourceType, Method: v1.OperationGet}, Method: http.MethodGet, Path: "/planes/aws/aws/accounts/0000000/regions/some-region/providers/AWS.Kinesis/locations/global/operationResults/00000000-0000-0000-0000-000000000000", }, { - OperationType: v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationStatuses}, + OperationType: v1.OperationType{Type: OperationStatusResourceType, Method: v1.OperationGet}, Method: http.MethodGet, Path: "/planes/aws/aws/accounts/0000000/regions/some-region/providers/AWS.Kinesis/locations/global/operationStatuses/00000000-0000-0000-0000-000000000000", }, diff --git a/pkg/ucp/integrationtests/testrp/async.go b/pkg/ucp/integrationtests/testrp/async.go index 6bf7249c52..4723b359e6 100644 --- a/pkg/ucp/integrationtests/testrp/async.go +++ b/pkg/ucp/integrationtests/testrp/async.go @@ -58,13 +58,10 @@ func AsyncResource(t *testing.T, ts *testserver.TestServer, rootScope string, pu resourceType := "System.Test/testResources" - operationStoreClient, err := ts.Clients.StorageProvider.GetStorageClient(ctx, "System.Test/operationStatuses") - require.NoError(t, err) - queueClient, err := ts.Clients.QueueProvider.GetClient(ctx) require.NoError(t, err) - statusManager := statusmanager.New(operationStoreClient, queueClient, "System.Test", v1.LocationGlobal) + statusManager := statusmanager.New(ts.Clients.StorageProvider, queueClient, v1.LocationGlobal) backendOpts := backend_ctrl.Options{ DataProvider: ts.Clients.StorageProvider, diff --git a/pkg/ucp/integrationtests/testserver/testserver.go b/pkg/ucp/integrationtests/testserver/testserver.go index 5fd3830d64..ecc862d00e 100644 --- a/pkg/ucp/integrationtests/testserver/testserver.go +++ b/pkg/ucp/integrationtests/testserver/testserver.go @@ -149,7 +149,7 @@ func StartWithMocks(t *testing.T, configureModules func(options modules.Options) AnyTimes() queueClient := queue.NewMockClient(ctrl) - queueProvider := queueprovider.New("System.Resources", queueprovider.QueueProviderOptions{}) + queueProvider := queueprovider.New(queueprovider.QueueProviderOptions{Name: "System.Resources"}) queueProvider.SetClient(queueClient) secretClient := secret.NewMockClient(ctrl) @@ -254,6 +254,7 @@ func StartWithETCD(t *testing.T, configureModules func(options modules.Options) ETCD: storageOptions.ETCD, } queueOptions := queueprovider.QueueProviderOptions{ + Name: "System.Resources", Provider: queueprovider.TypeInmemory, InMemory: &queueprovider.InMemoryQueueOptions{}, } @@ -262,7 +263,7 @@ func StartWithETCD(t *testing.T, configureModules func(options modules.Options) pathBase := "/" + uuid.New().String() dataProvider := dataprovider.NewStorageProvider(storageOptions) secretProvider := secretprovider.NewSecretProvider(secretOptions) - queueProvider := queueprovider.New("System.Resources", queueOptions) + queueProvider := queueprovider.New(queueOptions) router := chi.NewRouter() router.Use(servicecontext.ARMRequestCtx(pathBase, "global")) diff --git a/pkg/ucp/queue/provider/factory.go b/pkg/ucp/queue/provider/factory.go index 7cf1bc4efd..d3e9b3923d 100644 --- a/pkg/ucp/queue/provider/factory.go +++ b/pkg/ucp/queue/provider/factory.go @@ -30,18 +30,18 @@ import ( runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) -type factoryFunc func(context.Context, string, QueueProviderOptions) (queue.Client, error) +type factoryFunc func(context.Context, QueueProviderOptions) (queue.Client, error) var clientFactory = map[QueueProviderType]factoryFunc{ TypeInmemory: initInMemory, TypeAPIServer: initAPIServer, } -func initInMemory(ctx context.Context, name string, opt QueueProviderOptions) (queue.Client, error) { - return qinmem.NewNamedQueue(name), nil +func initInMemory(ctx context.Context, opt QueueProviderOptions) (queue.Client, error) { + return qinmem.NewNamedQueue(opt.Name), nil } -func initAPIServer(ctx context.Context, name string, opt QueueProviderOptions) (queue.Client, error) { +func initAPIServer(ctx context.Context, opt QueueProviderOptions) (queue.Client, error) { if opt.APIServer.Namespace == "" { return nil, errors.New("failed to initialize APIServer client: namespace is required") } @@ -76,7 +76,7 @@ func initAPIServer(ctx context.Context, name string, opt QueueProviderOptions) ( } return apiserver.New(rc, apiserver.Options{ - Name: name, + Name: opt.Name, Namespace: opt.APIServer.Namespace, }) } diff --git a/pkg/ucp/queue/provider/options.go b/pkg/ucp/queue/provider/options.go index 8c06d01985..116313d03a 100644 --- a/pkg/ucp/queue/provider/options.go +++ b/pkg/ucp/queue/provider/options.go @@ -21,6 +21,9 @@ type QueueProviderOptions struct { // Provider configures the storage provider. Provider QueueProviderType `yaml:"provider"` + // Name represents the unique name of queue. + Name string `yaml:"name"` + // InMemory represents inmemory queue client options. (Optional) InMemory *InMemoryQueueOptions `yaml:"inMemoryQueue,omitempty"` diff --git a/pkg/ucp/queue/provider/provider.go b/pkg/ucp/queue/provider/provider.go index 7d331e3dc6..b22418800c 100644 --- a/pkg/ucp/queue/provider/provider.go +++ b/pkg/ucp/queue/provider/provider.go @@ -22,7 +22,6 @@ import ( "sync" queue "github.com/radius-project/radius/pkg/ucp/queue/client" - "github.com/radius-project/radius/pkg/ucp/util" ) var ( @@ -31,7 +30,6 @@ var ( // QueueProvider is the provider to create and manage queue client. type QueueProvider struct { - name string options QueueProviderOptions queueClient queue.Client @@ -39,9 +37,8 @@ type QueueProvider struct { } // New creates new QueueProvider instance. -func New(name string, opts QueueProviderOptions) *QueueProvider { +func New(opts QueueProviderOptions) *QueueProvider { return &QueueProvider{ - name: util.NormalizeStringToLower(name), queueClient: nil, options: opts, } @@ -56,7 +53,7 @@ func (p *QueueProvider) GetClient(ctx context.Context) (queue.Client, error) { err := ErrUnsupportedStorageProvider p.once.Do(func() { if fn, ok := clientFactory[p.options.Provider]; ok { - p.queueClient, err = fn(ctx, p.name, p.options) + p.queueClient, err = fn(ctx, p.options) } }) diff --git a/pkg/ucp/queue/provider/provider_test.go b/pkg/ucp/queue/provider/provider_test.go index 839108557d..1a1bb49320 100644 --- a/pkg/ucp/queue/provider/provider_test.go +++ b/pkg/ucp/queue/provider/provider_test.go @@ -24,7 +24,8 @@ import ( ) func TestGetClient_ValidQueue(t *testing.T) { - p := New("Applications.Core", QueueProviderOptions{ + p := New(QueueProviderOptions{ + Name: "Applications.Core", Provider: TypeInmemory, InMemory: &InMemoryQueueOptions{}, }) @@ -38,7 +39,8 @@ func TestGetClient_ValidQueue(t *testing.T) { } func TestGetClient_InvalidQueue(t *testing.T) { - p := New("Applications.Core", QueueProviderOptions{ + p := New(QueueProviderOptions{ + Name: "Applications.Core", Provider: QueueProviderType("undefined"), }) diff --git a/pkg/ucp/server/server.go b/pkg/ucp/server/server.go index b13f2d1e93..85d9f6eab1 100644 --- a/pkg/ucp/server/server.go +++ b/pkg/ucp/server/server.go @@ -18,7 +18,6 @@ package server import ( "errors" - "flag" "fmt" "os" "strings" @@ -151,9 +150,6 @@ func NewServerOptionsFromEnvironment() (Options, error) { // NewServer creates a new hosting.Host instance with services for API, EmbeddedETCD, Metrics, Profiler and Backend (if // enabled) based on the given Options. func NewServer(options *Options) (*hosting.Host, error) { - var enableAsyncWorker bool - flag.BoolVar(&enableAsyncWorker, "enable-asyncworker", true, "Flag to run async request process worker (for private preview and dev/test purpose).") - hostingServices := []hosting.Service{ api.NewService(api.ServiceOptions{ ProviderName: UCPProviderName, @@ -191,19 +187,17 @@ func NewServer(options *Options) (*hosting.Host, error) { hostingServices = append(hostingServices, profilerservice.NewService(profilerOptions)) } - if enableAsyncWorker { - backendServiceOptions := hostOpts.HostOptions{ - Config: &hostOpts.ProviderConfig{ - StorageProvider: options.StorageProviderOptions, - SecretProvider: options.SecretProviderOptions, - QueueProvider: options.QueueProviderOptions, - MetricsProvider: options.MetricsProviderOptions, - TracerProvider: options.TracerProviderOptions, - ProfilerProvider: options.ProfilerProviderOptions, - }, - } - hostingServices = append(hostingServices, backend.NewService(backendServiceOptions)) + backendServiceOptions := hostOpts.HostOptions{ + Config: &hostOpts.ProviderConfig{ + StorageProvider: options.StorageProviderOptions, + SecretProvider: options.SecretProviderOptions, + QueueProvider: options.QueueProviderOptions, + MetricsProvider: options.MetricsProviderOptions, + TracerProvider: options.TracerProviderOptions, + ProfilerProvider: options.ProfilerProviderOptions, + }, } + hostingServices = append(hostingServices, backend.NewService(backendServiceOptions)) return &hosting.Host{ Services: hostingServices,