From 466aa21d2cdf6fe4d6939df93231fbdd8655471f Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Wed, 6 Mar 2024 13:09:23 -0800 Subject: [PATCH 1/2] wip Signed-off-by: Young Bu Park --- .../backend/containers/createorupdate.go | 424 ++++++++ pkg/corerp/backend/containers/delete.go | 60 ++ pkg/corerp/backend/containers/util.go | 122 +++ .../controller/containers/validator.go | 11 + pkg/platform-provider/k8sprovider/manifest.go | 262 +++++ pkg/platform-provider/k8sprovider/provider.go | 50 + pkg/platform-provider/k8sprovider/rbac.go | 86 ++ pkg/platform-provider/k8sprovider/render.go | 958 ++++++++++++++++++ pkg/platform-provider/k8sprovider/volumes.go | 43 + pkg/platform-provider/types.go | 60 ++ 10 files changed, 2076 insertions(+) create mode 100644 pkg/corerp/backend/containers/createorupdate.go create mode 100644 pkg/corerp/backend/containers/delete.go create mode 100644 pkg/corerp/backend/containers/util.go create mode 100644 pkg/platform-provider/k8sprovider/manifest.go create mode 100644 pkg/platform-provider/k8sprovider/provider.go create mode 100644 pkg/platform-provider/k8sprovider/rbac.go create mode 100644 pkg/platform-provider/k8sprovider/render.go create mode 100644 pkg/platform-provider/k8sprovider/volumes.go create mode 100644 pkg/platform-provider/types.go diff --git a/pkg/corerp/backend/containers/createorupdate.go b/pkg/corerp/backend/containers/createorupdate.go new file mode 100644 index 0000000000..8da55c05f6 --- /dev/null +++ b/pkg/corerp/backend/containers/createorupdate.go @@ -0,0 +1,424 @@ +/* +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 containers + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/corerp/datamodel" + platformprovider "github.com/radius-project/radius/pkg/platform-provider" + rpv1 "github.com/radius-project/radius/pkg/rp/v1" + "github.com/radius-project/radius/pkg/to" + "github.com/radius-project/radius/pkg/ucp/resources" + "github.com/radius-project/radius/pkg/ucp/store" + + ucp_radius "github.com/radius-project/radius/pkg/ucp/resources/radius" +) + +var _ ctrl.Controller = (*CreateOrUpdateResource)(nil) + +// RoleAssignmentData describes how to configure role assignment permissions based on the kind of +// connection. +type RoleAssignmentData struct { + // RoleNames contains the names of the IAM roles to grant. + RoleNames []string + ResourceType string +} + +type RoleAssignment struct { + ResourceID resources.ID + RoleNames []string +} + +// TODO: Move this to provider code. this is predefined roles +var roleAssignmentMap = map[datamodel.IAMKind]RoleAssignmentData{ + // Example of how to read this data: + // + // For a KeyVault connection... + // - Look up the dependency based on the connection.Source (azure.com.KeyVault) + // - Find the output resource matching LocalID of that dependency (Microsoft.KeyVault/vaults) + // - Apply the roles in RoleNames (Key Vault Secrets User, Key Vault Crypto User) + datamodel.KindAzureComKeyVault: { + ResourceType: "Microsoft.KeyVault/vaults", + RoleNames: []string{ + "Key Vault Secrets User", + "Key Vault Crypto User", + }, + }, + datamodel.KindAzure: { + // RBAC for non-Radius Azure resources. Supports user specified roles. + // More information can be found here: https://github.com/radius-project/radius/issues/1321 + }, +} + +// CreateOrUpdateResource is the async operation controller to create or update Applications.Core/Containers resource. +type CreateOrUpdateResource struct { + ctrl.BaseController +} + +type BaseResource struct { + v1.BaseResource + datamodel.PortableResourceMetadata + Properties rpv1.BasicResourceProperties +} + +// NewCreateOrUpdateResource creates a new CreateOrUpdateResource controller. +func NewCreateOrUpdateResource(opts ctrl.Options) (ctrl.Controller, error) { + return &CreateOrUpdateResource{ctrl.NewBaseAsyncController(opts)}, nil +} + +func isIdentitySupported(roles map[datamodel.IAMKind]RoleAssignmentData, kind datamodel.IAMKind) bool { + if roles == nil || !kind.IsValid() { + return false + } + + _, ok := roles[kind] + return ok +} + +func getConnectedResources(container *datamodel.ContainerResource) ([]resources.ID, error) { + properties := container.Properties + connectedResources := []resources.ID{} + + for _, connection := range properties.Connections { + // if source is a URL, it is valid (example: 'http://containerx:3000'). + if isURL(connection.Source) { + continue + } + + // If source is not a URL, it must be either resource ID, invalid string, or empty (example: containerhttproute.id). + rID, err := resources.ParseResource(connection.Source) + if err != nil { + return nil, fmt.Errorf("invalid source: %s. Must be either a URL or a valid resourceID", connection.Source) + } + + if ucp_radius.IsRadiusResource(rID) { + connectedResources = append(connectedResources, rID) + } + } + + for _, port := range properties.Container.Ports { + if port.Provides == "" { + continue + } + + rID, err := resources.ParseResource(port.Provides) + if err != nil { + return nil, err + } + + if ucp_radius.IsRadiusResource(rID) { + connectedResources = append(connectedResources, rID) + } + } + + for _, vol := range properties.Container.Volumes { + switch vol.Kind { + case datamodel.Persistent: + rID, err := resources.ParseResource(vol.Persistent.Source) + if err != nil { + return nil, err + } + + if ucp_radius.IsRadiusResource(rID) { + connectedResources = append(connectedResources, rID) + } + } + } + + return connectedResources, nil +} + +type EnvVar struct { + Name string + Value []byte + IsSecret bool +} + +func getEnvironmentVariables(resource *datamodel.ContainerResource, resourceMap map[string]*BaseResource) ([]EnvVar, error) { + env := []EnvVar{} + properties := resource.Properties + + // Take each connection and create environment variables for each part + // We'll store each value in a secret named with the same name as the resource. + // We'll use the environment variable names as keys. + // Float is used by the JSON serializer + for name, con := range properties.Connections { + dep := resourceMap[con.Source] + + if !con.GetDisableDefaultEnvVars() { + source := con.Source + if source == "" { + continue + } + + // handles case where container has source field structured as a URL. + if isURL(source) { + // parse source into scheme, hostname, and port. + scheme, hostname, port, err := parseURL(source) + if err != nil { + return nil, fmt.Errorf("failed to parse source URL: %w", err) + } + + schemeKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "SCHEME") + hostnameKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "HOSTNAME") + portKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "PORT") + + env = append(env, EnvVar{Name: schemeKey, Value: []byte(scheme)}) + env = append(env, EnvVar{Name: hostnameKey, Value: []byte(hostname)}) + env = append(env, EnvVar{Name: portKey, Value: []byte(port)}) + continue + } + + // handles case where container has source field structured as a resourceID. + for key, value := range dep.ComputedValues { + name := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), strings.ToUpper(key)) + switch v := value.(type) { + case string: + env = append(env, EnvVar{Name: name, Value: []byte(v)}) + case float64: + env = append(env, EnvVar{Name: name, Value: []byte(strconv.Itoa(int(v)))}) + case int: + env = append(env, EnvVar{Name: name, Value: []byte(strconv.Itoa(v))}) + } + } + } + } + + return env, nil +} + +// Run checks if the resource exists, renders the resource, deploys the resource, applies the +// deployment output to the resource, deletes any resources that are no longer needed, and saves the resource. +func (c *CreateOrUpdateResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.Result, error) { + var platform platformprovider.Provider + + obj, err := c.StorageClient().Get(ctx, request.ResourceID) + if err != nil && !errors.Is(&store.ErrNotFound{ID: request.ResourceID}, err) { + return ctrl.Result{}, err + } + + container, ok := obj.Data.(*datamodel.ContainerResource) + if !ok { + return ctrl.NewFailedResult(v1.ErrorDetails{Code: "IntervalError", Message: "invalid datamodel"}), nil + } + + properties := container.Properties + appId, err := resources.ParseResource(properties.Application) + if err != nil { + return ctrl.Result{}, err + } + + // TODO: Get environment / application resources + + // TODO: Moved the following code to frontend. + // Preserve outputresources. + outputResources := []rpv1.OutputResource{} + for _, rr := range properties.Resources { + id, err := resources.Parse(rr.ID) + if err != nil { + return ctrl.Result{}, err + } + + outputResources = append(outputResources, rpv1.OutputResource{ID: id, RadiusManaged: to.Ptr(false)}) + } + + // TODO: It must check if the old resource is ContainerResourceProvisioningManual + if properties.ResourceProvisioning == datamodel.ContainerResourceProvisioningManual { + // Do nothing! This is a manual resource. + return ctrl.Result{}, nil + } + + // Get all connected resources. + connectedResources, err := getConnectedResources(container) + if err != nil { + return ctrl.Result{}, err + } + + connectedResourceMap := map[string]*BaseResource{} + for _, id := range connectedResources { + sc, err := c.DataProvider().GetStorageClient(ctx, id.Type()) + if err != nil { + return ctrl.Result{}, err + } + + obj, err := sc.Get(ctx, id.String()) + r := &BaseResource{} + if err = obj.As(r); err != nil { + return ctrl.Result{}, err + } + connectedResourceMap[id.String()] = r + } + + // Original code: Collect radius resource from container.Volumes and container.Ports[0].Provides + + // This is the flag to create the service route. + isRouteRequired := false + + for portName, port := range properties.Container.Ports { + // if the container has an exposed port, note that down. + // A single service will be generated for a container with one or more exposed ports. + if port.ContainerPort == 0 { + return ctrl.Result{}, fmt.Errorf("invalid ports definition: must define a ContainerPort, but ContainerPort is: %d.", port.ContainerPort) + } + + if port.Port == 0 { + port.Port = port.ContainerPort + properties.Container.Ports[portName] = port + } + + // if the container has an exposed port, but no 'provides' field, it requires DNS service generation. + if port.Provides == "" { + isRouteRequired = true + } + } + + // Get role assignments for each connections. + roles := []RoleAssignment{} + for _, connection := range properties.Connections { + sourceID, err := resources.Parse(connection.Source) + if err != nil { + return ctrl.Result{}, err + } + + predefined, ok := roleAssignmentMap[connection.IAM.Kind] + if ok { + dependency, ok := connectedResourceMap[connection.Source] + if !ok { + return ctrl.Result{}, fmt.Errorf("dependency not found: %s", connection.Source) + } + + var foundRoles *RoleAssignment + for _, outputResource := range dependency.Properties.Status.OutputResources { + if outputResource.ID.Type() == predefined.ResourceType { + foundRoles = &RoleAssignment{ + ResourceID: sourceID, + RoleNames: predefined.RoleNames, + } + break + } + } + + if foundRoles != nil { + roles = append(roles, *foundRoles) + } + } else { + if len(connection.IAM.Roles) > 0 && connection.Source != "" { + roles = append(roles, RoleAssignment{ + ResourceID: sourceID, + RoleNames: connection.IAM.Roles, + }) + } + } + } + + // Get env vars and secret data + envVars, err := getEnvironmentVariables(container, connectedResourceMap) + + // Get volumes and if Azure Keyvault volume is found, add AzureKeyVaultSecretsUserRole, AzureKeyVaultCryptoUserRole to "roles". + for volName, volProperties := range properties.Container.Volumes { + if volProperties.Kind == datamodel.Persistent { + volumeResource, ok := connectedResourceMap[volProperties.Persistent.Source] + if !ok { + return ctrl.Result{}, fmt.Errorf("volume resource not found: %s", volProperties.Persistent.Source) + } + + if volumeResource.Properties.Kind == datamodel.AzureKeyVaultVolume { + // Add the roles to the roles list. + roles = append(roles, RoleAssignment{ + ResourceID: resources.ID(volumeProperties.Persistent.Source), + RoleNames: []string{"AzureKeyVaultSecretsUserRole", "AzureKeyVaultCryptoUserRole"}, + }) + } + } + } + + // Call secretstore to create secret for connections' secrets. + secretStoreProvider, err := platform.SecretStore() + err = secretStoreProvider.CreateOrUpdateSecretStore(ctx, &datamodel.SecretStoreProperties{}) + + for _, role := range roles { + if role.ResourceID.PlaneNamespace() == "azure" { + // CreateOrUpdate identity for cloud iam. (Azure managed identity) + identity, err := platform.Identity() + rID, err := identity.CreateOrUpdateIdentity(ctx, &datamodel.IdentityProperties{}) + if err != nil { + return ctrl.Result{}, err + } + // CreateOrUpdate identity role binding for cloud iam. (Azure managed identity) for connection resources. + err = identity.AssignRoleToIdentity(ctx, rID, role.ResourceID, role.RoleNames) + } + } + + // CreateOrUpdate identity for cloud iam. (Azure managed identity) + identity, err := platform.Identity() + rID, err := identity.CreateOrUpdateIdentity(ctx, &datamodel.IdentityProperties{}) + if err != nil { + return ctrl.Result{}, err + } + + // CreateOrUpdate identity role binding for container platform. (Service Account) + err = identity.AssignRoleToIdentity(ctx, rID, []string{"Service Account"}) + + // CreateOrUpdate the container. + containerProvider, err := platform.Container() + if err != nil { + return ctrl.Result{}, err + } + + err = containerProvider.CreateOrUpdateContainer(ctx, container) + if err != nil { + return ctrl.Result{}, err + } + + routeProvider, err := platform.Route() + if err != nil { + return ctrl.Result{}, err + } + + if isRouteRequired { + err = routeProvider.CreateOrUpdateRoute(ctx, properties.Container.Ports) + if err != nil { + return ctrl.Result{}, err + } + } + + // CreateOrUpdate the routes in the container. + + /* + # Save output resource. + + nr := &store.Object{ + Metadata: store.Metadata{ + ID: request.ResourceID, + }, + Data: deploymentDataModel, + } + err = c.StorageClient().Save(ctx, nr, store.WithETag(obj.ETag)) + if err != nil { + return ctrl.Result{}, err + } + */ + + return ctrl.Result{}, nil +} diff --git a/pkg/corerp/backend/containers/delete.go b/pkg/corerp/backend/containers/delete.go new file mode 100644 index 0000000000..6744a1a479 --- /dev/null +++ b/pkg/corerp/backend/containers/delete.go @@ -0,0 +1,60 @@ +/* +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 containers + +import ( + "context" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" + "github.com/radius-project/radius/pkg/ucp/resources" +) + +var _ ctrl.Controller = (*DeleteResource)(nil) + +// DeleteResource is the async operation controller to delete Applications.Core/Containers resource. +type DeleteResource struct { + ctrl.BaseController +} + +// NewDeleteResource creates a new DeleteResource controller with the given options. +func NewDeleteResource(opts ctrl.Options) (ctrl.Controller, error) { + return &DeleteResource{ctrl.NewBaseAsyncController(opts)}, nil +} + +// Run retrieves a resource from storage, parses its ID, gets its data model, converts it to a deployment +// data model, deletes the resource from the deployment processor, and deletes the resource from storage. +// It returns an error if any of these steps fail. +func (c *DeleteResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.Result, error) { + _, err := c.StorageClient().Get(ctx, request.ResourceID) + if err != nil { + return ctrl.NewFailedResult(v1.ErrorDetails{Message: err.Error()}), err + } + + // This code is general and we might be processing an async job for a resource or a scope, so using the general Parse function. + _, err = resources.Parse(request.ResourceID) + if err != nil { + return ctrl.Result{}, err + } + + err = c.StorageClient().Delete(ctx, request.ResourceID) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, err +} diff --git a/pkg/corerp/backend/containers/util.go b/pkg/corerp/backend/containers/util.go new file mode 100644 index 0000000000..5fc631419c --- /dev/null +++ b/pkg/corerp/backend/containers/util.go @@ -0,0 +1,122 @@ +package containers + +import ( + "context" + "fmt" + "net" + "net/url" + "sort" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/corerp/datamodel" + "github.com/radius-project/radius/pkg/ucp/resources" + resources_radius "github.com/radius-project/radius/pkg/ucp/resources/radius" + corev1 "k8s.io/api/core/v1" +) + +func GetDependencyIDs(ctx context.Context, resource *datamodel.ContainerResource) (radiusResourceIDs []resources.ID, azureResourceIDs []resources.ID, err error) { + properties := resource.Properties + + // Right now we only have things in connections and ports as rendering dependencies - we'll add more things + // in the future... eg: volumes + // + // Anywhere we accept a resource ID in the model should have its value returned from here + + // ensure that users cannot use DNS-SD and httproutes simultaneously. + for _, connection := range properties.Connections { + if isURL(connection.Source) { + continue + } + + // if the source is not a URL, it either a resourceID or invalid. + resourceID, err := resources.ParseResource(connection.Source) + if err != nil { + return nil, nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid source: %s. Must be either a URL or a valid resourceID", connection.Source)) + } + + // Non-radius Azure connections that are accessible from Radius container resource. + if connection.IAM.Kind.IsKind(datamodel.KindAzure) { + azureResourceIDs = append(azureResourceIDs, resourceID) + continue + } + + if resources_radius.IsRadiusResource(resourceID) { + radiusResourceIDs = append(radiusResourceIDs, resourceID) + continue + } + } + + for _, port := range properties.Container.Ports { + provides := port.Provides + + // if provides is empty, skip this port. A service for this port will be generated later on. + if provides == "" { + continue + } + + resourceID, err := resources.ParseResource(provides) + if err != nil { + return nil, nil, v1.NewClientErrInvalidRequest(err.Error()) + } + + if resources_radius.IsRadiusResource(resourceID) { + radiusResourceIDs = append(radiusResourceIDs, resourceID) + continue + } + } + + for _, volume := range properties.Container.Volumes { + switch volume.Kind { + case datamodel.Persistent: + resourceID, err := resources.ParseResource(volume.Persistent.Source) + if err != nil { + return nil, nil, v1.NewClientErrInvalidRequest(err.Error()) + } + + if resources_radius.IsRadiusResource(resourceID) { + radiusResourceIDs = append(radiusResourceIDs, resourceID) + continue + } + } + } + + return radiusResourceIDs, azureResourceIDs, nil +} + +func getSortedKeys(env map[string]corev1.EnvVar) []string { + keys := []string{} + for k := range env { + key := k + keys = append(keys, key) + } + + sort.Strings(keys) + return keys +} + +func isURL(input string) bool { + _, err := url.ParseRequestURI(input) + + // if first character is a slash, it's not a URL. It's a path. + if input == "" || err != nil || input[0] == '/' { + return false + } + return true +} + +func parseURL(sourceURL string) (scheme, hostname, port string, err error) { + u, err := url.Parse(sourceURL) + if err != nil { + return "", "", "", err + } + + scheme = u.Scheme + host := u.Host + + hostname, port, err = net.SplitHostPort(host) + if err != nil { + return "", "", "", err + } + + return scheme, hostname, port, nil +} diff --git a/pkg/corerp/frontend/controller/containers/validator.go b/pkg/corerp/frontend/controller/containers/validator.go index 00a75dfc40..6e9afe80c6 100644 --- a/pkg/corerp/frontend/controller/containers/validator.go +++ b/pkg/corerp/frontend/controller/containers/validator.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "net/url" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -38,6 +39,16 @@ const ( podTargetProperty = "$.properties.runtimes.kubernetes.pod" ) +func isURL(input string) bool { + _, err := url.ParseRequestURI(input) + + // if first character is a slash, it's not a URL. It's a path. + if input == "" || err != nil || input[0] == '/' { + return false + } + return true +} + // ValidateAndMutateRequest checks if the newResource has a user-defined identity and if so, returns a bad request // response, otherwise it sets the identity of the newResource to the identity of the oldResource if it exists. func ValidateAndMutateRequest(ctx context.Context, newResource, oldResource *datamodel.ContainerResource, options *controller.Options) (rest.Response, error) { diff --git a/pkg/platform-provider/k8sprovider/manifest.go b/pkg/platform-provider/k8sprovider/manifest.go new file mode 100644 index 0000000000..df358ccc84 --- /dev/null +++ b/pkg/platform-provider/k8sprovider/manifest.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 k8sprovider + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/radius-project/radius/pkg/corerp/datamodel" + "github.com/radius-project/radius/pkg/corerp/renderers" + "github.com/radius-project/radius/pkg/kubernetes" + "github.com/radius-project/radius/pkg/kubeutil" + rpv1 "github.com/radius-project/radius/pkg/rp/v1" + "github.com/radius-project/radius/pkg/ucp/ucplog" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/strategicpatch" +) + +var errDeploymentNotFound = errors.New("deployment resource must be in outputResources") + +// fetchBaseManifest fetches the base manifest from the container resource. +func fetchBaseManifest(r *datamodel.ContainerResource) (kubeutil.ObjectManifest, error) { + baseManifest := kubeutil.ObjectManifest{} + runtimes := r.Properties.Runtimes + var err error + + if runtimes != nil && runtimes.Kubernetes != nil && runtimes.Kubernetes.Base != "" { + baseManifest, err = kubeutil.ParseManifest([]byte(runtimes.Kubernetes.Base)) + if err != nil { + return nil, err + } + } + + return baseManifest, nil +} + +// getDeploymentBase returns the deployment resource based on the given base manifest. +// If the container has a base manifest, get the deployment resource from the base manifest. +// Otherwise, populate default resources. +func getDeploymentBase(manifest kubeutil.ObjectManifest, appName string, r *datamodel.ContainerResource, options *renderers.RenderOptions) *appsv1.Deployment { + name := kubernetes.NormalizeResourceName(r.Name) + + defaultDeployment := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: name, + }, + }, + }, + }, + }, + } + + if resource := manifest.GetFirst(appsv1.SchemeGroupVersion.WithKind("Deployment")); resource != nil { + defaultDeployment = resource.(*appsv1.Deployment) + } + + defaultDeployment.ObjectMeta = getObjectMeta(defaultDeployment.ObjectMeta, appName, r.Name, r.ResourceTypeName(), *options) + if defaultDeployment.Spec.Selector == nil { + defaultDeployment.Spec.Selector = &metav1.LabelSelector{} + } + + podTemplate := &defaultDeployment.Spec.Template + if podTemplate.ObjectMeta.Labels == nil { + podTemplate.ObjectMeta.Labels = map[string]string{} + } + + if podTemplate.ObjectMeta.Annotations == nil { + podTemplate.ObjectMeta.Annotations = map[string]string{} + } + + if len(podTemplate.Spec.Containers) == 0 { + podTemplate.Spec.Containers = []corev1.Container{} + } + + found := false + for _, container := range podTemplate.Spec.Containers { + if strings.EqualFold(container.Name, name) { + found = true + break + } + } + if !found { + podTemplate.Spec.Containers = append(podTemplate.Spec.Containers, corev1.Container{Name: name}) + } + + return defaultDeployment +} + +// getServiceBase returns the service resource based on the given base manifest. +// If the service has a base manifest, get the service resource from the base manifest. +// Otherwise, populate default resources. +func getServiceBase(manifest kubeutil.ObjectManifest, appName string, r *datamodel.ContainerResource, options *renderers.RenderOptions) *corev1.Service { + defaultService := &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{}, + Type: corev1.ServiceTypeClusterIP, + }, + } + if resource := manifest.GetFirst(corev1.SchemeGroupVersion.WithKind("Service")); resource != nil { + defaultService = resource.(*corev1.Service) + } + defaultService.ObjectMeta = getObjectMeta(defaultService.ObjectMeta, appName, r.Name, r.ResourceTypeName(), *options) + return defaultService +} + +// getServiceAccountBase returns the service account resource based on the given base manifest. +// If the service account has a base manifest, get the service account resource from the base manifest. +// Otherwise, populate default resources. +func getServiceAccountBase(manifest kubeutil.ObjectManifest, appName string, r *datamodel.ContainerResource, options *renderers.RenderOptions) *corev1.ServiceAccount { + defaultAccount := &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + } + + if resource := manifest.GetFirst(corev1.SchemeGroupVersion.WithKind("ServiceAccount")); resource != nil { + defaultAccount = resource.(*corev1.ServiceAccount) + } + + defaultAccount.ObjectMeta = getObjectMeta(defaultAccount.ObjectMeta, appName, r.Name, r.ResourceTypeName(), *options) + + return defaultAccount +} + +// populateAllBaseResources populates all remaining resources from manifest into outputResources. +// These resources must be deployed before Deployment resource by adding them as a dependency. +func populateAllBaseResources(ctx context.Context, base kubeutil.ObjectManifest, outputResources []rpv1.OutputResource, options renderers.RenderOptions) []rpv1.OutputResource { + logger := ucplog.FromContextOrDiscard(ctx) + + // Find deployment resource from outputResources to add base manifest resources as a dependency. + var deploymentResource *rpv1.Resource + for _, r := range outputResources { + if r.LocalID == rpv1.LocalIDDeployment { + deploymentResource = r.CreateResource + break + } + } + + // This should not happen because deployment resource is created in the first place. + if deploymentResource == nil { + panic(errDeploymentNotFound) + } + + // Populate the remaining objects in base manifest into outputResources. + // These resources must be deployed before Deployment resource by adding them as a dependency. + for k, resources := range base { + localIDPrefix := "" + + switch k { + case corev1.SchemeGroupVersion.WithKind("Secret"): + localIDPrefix = rpv1.LocalIDSecret + case corev1.SchemeGroupVersion.WithKind("ConfigMap"): + localIDPrefix = rpv1.LocalIDConfigMap + + default: + continue + } + + for _, resource := range resources { + objMeta := resource.(metav1.ObjectMetaAccessor).GetObjectMeta().(*metav1.ObjectMeta) + objMeta.Namespace = options.Environment.Namespace + logger.Info(fmt.Sprintf("Adding base manifest resource, kind: %s, name: %s", k, objMeta.Name)) + + localID := rpv1.NewLocalID(localIDPrefix, objMeta.Name) + o := rpv1.NewKubernetesOutputResource(localID, resource, *objMeta) + deploymentResource.Dependencies = append(deploymentResource.Dependencies, localID) + outputResources = append(outputResources, o) + } + } + + return outputResources +} + +func patchPodSpec(sourceSpec *corev1.PodSpec, patchSpec []byte) (*corev1.PodSpec, error) { + podSpecJSON, err := json.Marshal(sourceSpec) + if err != nil { + return nil, err + } + + merged, err := strategicpatch.StrategicMergePatch(podSpecJSON, patchSpec, corev1.PodSpec{}) + if err != nil { + return nil, err + } + + patched := &corev1.PodSpec{} + err = json.Unmarshal(merged, patched) + if err != nil { + return nil, err + } + + return patched, nil +} + +func mergeLabelSelector(base *metav1.LabelSelector, cur *metav1.LabelSelector) *metav1.LabelSelector { + if base == nil { + base = &metav1.LabelSelector{} + } + + return &metav1.LabelSelector{ + MatchLabels: labels.Merge(base.MatchLabels, cur.MatchLabels), + MatchExpressions: append(base.MatchExpressions, cur.MatchExpressions...), + } +} + +func mergeObjectMeta(base metav1.ObjectMeta, cur metav1.ObjectMeta) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: cur.Name, + Namespace: cur.Namespace, + Labels: labels.Merge(base.Labels, cur.Labels), + Annotations: labels.Merge(base.Annotations, cur.Annotations), + } +} + +func getObjectMeta(base metav1.ObjectMeta, appName, resourceName, resourceType string, options renderers.RenderOptions) metav1.ObjectMeta { + cur := metav1.ObjectMeta{ + Name: kubernetes.NormalizeResourceName(resourceName), + Namespace: options.Environment.Namespace, + Labels: renderers.GetLabels(options, appName, resourceName, resourceType), + Annotations: renderers.GetAnnotations(options), + } + + return mergeObjectMeta(base, cur) +} diff --git a/pkg/platform-provider/k8sprovider/provider.go b/pkg/platform-provider/k8sprovider/provider.go new file mode 100644 index 0000000000..679d22dec1 --- /dev/null +++ b/pkg/platform-provider/k8sprovider/provider.go @@ -0,0 +1,50 @@ +package k8sprovider + +import ( + "context" + + "github.com/radius-project/radius/pkg/corerp/datamodel" + platformprovider "github.com/radius-project/radius/pkg/platform-provider" +) + +var _ platformprovider.Provider = (*KubeProvider)(nil) +var _ platformprovider.ContainerProvider = (*KubeProvider)(nil) + +type KubeProvider struct { +} + +func (p *KubeProvider) Initialize() error { + return nil +} + +func (p *KubeProvider) Name() string { + return "k8s" +} + +func (p *KubeProvider) Container() (platformprovider.ContainerProvider, error) { + return nil, nil +} + +func (p *KubeProvider) Route() (platformprovider.RouteProvider, error) { + return nil, nil +} + +func (p *KubeProvider) Gateway() (platformprovider.GatewayProvider, error) { + return nil, nil +} + +func (p *KubeProvider) Identity() (platformprovider.IdentityProvider, error) { + return nil, nil +} + +func (p *KubeProvider) Volume() (platformprovider.VolumeProvider, error) { + return nil, nil +} + +func (p *KubeProvider) SecretStore() (platformprovider.SecretStoreProvider, error) { + return nil, nil +} + +func (p *KubeProvider) CreateOrUpdateContainer(ctx context.Context, container *datamodel.ContainerResource) error { + return nil +} diff --git a/pkg/platform-provider/k8sprovider/rbac.go b/pkg/platform-provider/k8sprovider/rbac.go new file mode 100644 index 0000000000..ff6de46267 --- /dev/null +++ b/pkg/platform-provider/k8sprovider/rbac.go @@ -0,0 +1,86 @@ +/* +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 k8sprovider + +import ( + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/radius-project/radius/pkg/corerp/datamodel" + "github.com/radius-project/radius/pkg/kubernetes" + rpv1 "github.com/radius-project/radius/pkg/rp/v1" +) + +func makeRBACRole(appName, name, namespace string, resource *datamodel.ContainerResource) *rpv1.OutputResource { + labels := kubernetes.MakeDescriptiveLabels(appName, resource.Name, resource.Type) + + role := &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: kubernetes.NormalizeResourceName(name), + Namespace: namespace, + Labels: labels, + }, + // At this time, we support only secret rbac permission for the namespace. + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "list"}, + }, + }, + } + + or := rpv1.NewKubernetesOutputResource(rpv1.LocalIDKubernetesRole, role, role.ObjectMeta) + + return &or +} + +func makeRBACRoleBinding(appName, name, saName, namespace string, resource *datamodel.ContainerResource) *rpv1.OutputResource { + labels := kubernetes.MakeDescriptiveLabels(appName, resource.Name, resource.Type) + + bindings := &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: "rbac.authorization.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: kubernetes.NormalizeResourceName(name), + Namespace: namespace, + Labels: labels, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: kubernetes.NormalizeResourceName(name), + APIGroup: "rbac.authorization.k8s.io", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: saName, + }, + }, + } + + or := rpv1.NewKubernetesOutputResource(rpv1.LocalIDKubernetesRoleBinding, bindings, bindings.ObjectMeta) + + or.CreateResource.Dependencies = []string{rpv1.LocalIDKubernetesRole} + return &or +} diff --git a/pkg/platform-provider/k8sprovider/render.go b/pkg/platform-provider/k8sprovider/render.go new file mode 100644 index 0000000000..d90865d050 --- /dev/null +++ b/pkg/platform-provider/k8sprovider/render.go @@ -0,0 +1,958 @@ +/* +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 k8sprovider + +import ( + "context" + "errors" + "fmt" + "net" + "net/url" + "sort" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/corerp/datamodel" + "github.com/radius-project/radius/pkg/corerp/handlers" + "github.com/radius-project/radius/pkg/corerp/renderers" + azrenderer "github.com/radius-project/radius/pkg/corerp/renderers/container/azure" + azvolrenderer "github.com/radius-project/radius/pkg/corerp/renderers/volume/azure" + "github.com/radius-project/radius/pkg/kubernetes" + "github.com/radius-project/radius/pkg/kubeutil" + "github.com/radius-project/radius/pkg/resourcemodel" + rpv1 "github.com/radius-project/radius/pkg/rp/v1" + "github.com/radius-project/radius/pkg/to" + "github.com/radius-project/radius/pkg/ucp/resources" + resources_azure "github.com/radius-project/radius/pkg/ucp/resources/azure" + resources_radius "github.com/radius-project/radius/pkg/ucp/resources/radius" +) + +const ( + ResourceType = "Applications.Core/containers" + + // Liveness/Readiness constants + DefaultInitialDelaySeconds = 0 + DefaultFailureThreshold = 3 + DefaultPeriodSeconds = 10 + DefaultTimeoutSeconds = 5 + + AzureKeyVaultSecretsUserRole = "Key Vault Secrets User" + AzureKeyVaultCryptoUserRole = "Key Vault Crypto User" +) + +// GetSupportedKinds returns a list of supported volume kinds. +func GetSupportedKinds() []string { + keys := []string{} + keys = append(keys, datamodel.AzureKeyVaultVolume) + return keys +} + +// Renderer is the WorkloadRenderer implementation for containerized workload. +type Renderer struct { + // RoleAssignmentMap is an optional map of connection kind -> []Role Assignment. Used to configure managed + // identity permissions for cloud resources. This will be nil in environments that don't support role assignments. + RoleAssignmentMap map[datamodel.IAMKind]RoleAssignmentData +} + +// GetDependencyIDs parses the connections, ports and volumes of a container resource to return the Radius and Azure +// resource IDs. +func (r Renderer) GetDependencyIDs(ctx context.Context, dm v1.DataModelInterface) (radiusResourceIDs []resources.ID, azureResourceIDs []resources.ID, err error) { + resource, ok := dm.(*datamodel.ContainerResource) + if !ok { + return nil, nil, v1.ErrInvalidModelConversion + } + properties := resource.Properties + + // Right now we only have things in connections and ports as rendering dependencies - we'll add more things + // in the future... eg: volumes + // + // Anywhere we accept a resource ID in the model should have its value returned from here + + // ensure that users cannot use DNS-SD and httproutes simultaneously. + for _, connection := range properties.Connections { + if isURL(connection.Source) { + continue + } + + // if the source is not a URL, it either a resourceID or invalid. + resourceID, err := resources.ParseResource(connection.Source) + if err != nil { + return nil, nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid source: %s. Must be either a URL or a valid resourceID", connection.Source)) + } + + // Non-radius Azure connections that are accessible from Radius container resource. + if connection.IAM.Kind.IsKind(datamodel.KindAzure) { + azureResourceIDs = append(azureResourceIDs, resourceID) + continue + } + + if resources_radius.IsRadiusResource(resourceID) { + radiusResourceIDs = append(radiusResourceIDs, resourceID) + continue + } + } + + for _, port := range properties.Container.Ports { + provides := port.Provides + + // if provides is empty, skip this port. A service for this port will be generated later on. + if provides == "" { + continue + } + + resourceID, err := resources.ParseResource(provides) + if err != nil { + return nil, nil, v1.NewClientErrInvalidRequest(err.Error()) + } + + if resources_radius.IsRadiusResource(resourceID) { + radiusResourceIDs = append(radiusResourceIDs, resourceID) + continue + } + } + + for _, volume := range properties.Container.Volumes { + switch volume.Kind { + case datamodel.Persistent: + resourceID, err := resources.ParseResource(volume.Persistent.Source) + if err != nil { + return nil, nil, v1.NewClientErrInvalidRequest(err.Error()) + } + + if resources_radius.IsRadiusResource(resourceID) { + radiusResourceIDs = append(radiusResourceIDs, resourceID) + continue + } + } + } + + return radiusResourceIDs, azureResourceIDs, nil +} + +// Render creates role assignments, a deployment, and a secret for a given container resource, and returns a +// RendererOutput containing the resources and computed values. +func (r Renderer) Render(ctx context.Context, dm v1.DataModelInterface, options renderers.RenderOptions) (renderers.RendererOutput, error) { + resource, ok := dm.(*datamodel.ContainerResource) + if !ok { + return renderers.RendererOutput{}, v1.ErrInvalidModelConversion + } + + properties := resource.Properties + + appId, err := resources.ParseResource(properties.Application) + if err != nil { + return renderers.RendererOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid application id: %s ", err.Error())) + } + + outputResources := []rpv1.OutputResource{} + for _, rr := range properties.Resources { + id, err := resources.Parse(rr.ID) + if err != nil { + return renderers.RendererOutput{}, err + } + + outputResources = append(outputResources, rpv1.OutputResource{ID: id, RadiusManaged: to.Ptr(false)}) + } + + if properties.ResourceProvisioning == datamodel.ContainerResourceProvisioningManual { + // Do nothing! This is a manual resource. + return renderers.RendererOutput{Resources: outputResources}, nil + } + + ////////////---------------HERE---------------- + + // this flag is used to indicate whether or not this resource needs a service to be generated. + // this flag is triggered when a container has an exposed port(s), but no 'provides' field. + var needsServiceGeneration = false + + // check if connections are valid + for _, connection := range properties.Connections { + // if source is a URL, it is valid (example: 'http://containerx:3000'). + if isURL(connection.Source) { + continue + } + + // If source is not a URL, it must be either resource ID, invalid string, or empty (example: containerhttproute.id). + _, err := resources.ParseResource(connection.Source) + if err != nil { + return renderers.RendererOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid source: %s. Must be either a URL or a valid resourceID", connection.Source)) + } + } + + for portName, port := range properties.Container.Ports { + // if the container has an exposed port, note that down. + // A single service will be generated for a container with one or more exposed ports. + if port.ContainerPort == 0 { + return renderers.RendererOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid ports definition: must define a ContainerPort, but ContainerPort is: %d.", port.ContainerPort)) + } + + if port.Port == 0 { + port.Port = port.ContainerPort + properties.Container.Ports[portName] = port + } + + // if the container has an exposed port, but no 'provides' field, it requires DNS service generation. + if port.Provides == "" { + needsServiceGeneration = true + } + } + + dependencies := options.Dependencies + + // Connections might require a role assignment to grant access. + // THISTHISTHIS + roles := []rpv1.OutputResource{} + for _, connection := range properties.Connections { + if !r.isIdentitySupported(connection.IAM.Kind) { + continue + } + + rbacOutputResources, err := r.makeRoleAssignmentsForResource(ctx, &connection, dependencies) + if err != nil { + return renderers.RendererOutput{}, err + } + + roles = append(roles, rbacOutputResources...) + } + + if len(roles) > 0 { + outputResources = append(outputResources, roles...) + } + + // If the container has a base manifest, deserialize base manifest and validation should be done by frontend controller. + baseManifest, err := fetchBaseManifest(resource) + if err != nil { + return renderers.RendererOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid base manifest: %s", err.Error())) + } + + computedValues := map[string]rpv1.ComputedValueReference{} + + // Create the deployment as the primary workload + deploymentResources, secretData, err := r.makeDeployment(ctx, baseManifest, appId.Name(), options, computedValues, resource, roles) + if err != nil { + return renderers.RendererOutput{}, err + } + outputResources = append(outputResources, deploymentResources...) + + // If there are secrets we'll use a Kubernetes secret to hold them. This is already referenced + // by the deployment. + if len(secretData) > 0 { + outputResources = append(outputResources, r.makeSecret(ctx, *resource, appId.Name(), secretData, options)) + } + + var servicePorts []corev1.ServicePort + + // If the container has an exposed port and uses DNS-SD, generate a service for it. + if needsServiceGeneration { + for portName, port := range resource.Properties.Container.Ports { + // store portNames and portValues for use in service generation. + servicePort := corev1.ServicePort{ + Name: portName, + Port: port.Port, + TargetPort: intstr.FromInt(int(port.ContainerPort)), + Protocol: corev1.ProtocolTCP, + } + servicePorts = append(servicePorts, servicePort) + } + + // if a container has an exposed port, then we need to create a service for it. + basesrv := getServiceBase(baseManifest, appId.Name(), resource, &options) + serviceResource, err := r.makeService(basesrv, resource, options, ctx, servicePorts) + if err != nil { + return renderers.RendererOutput{}, err + } + outputResources = append(outputResources, serviceResource) + } + + // Populate the remaining resources from the base manifest. + outputResources = populateAllBaseResources(ctx, baseManifest, outputResources, options) + + return renderers.RendererOutput{ + Resources: outputResources, + ComputedValues: computedValues, + }, nil +} + +func (r Renderer) makeService(base *corev1.Service, resource *datamodel.ContainerResource, options renderers.RenderOptions, ctx context.Context, servicePorts []corev1.ServicePort) (rpv1.OutputResource, error) { + appId, err := resources.ParseResource(resource.Properties.Application) + if err != nil { + return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid application id: %s. id: %s", err.Error(), resource.Properties.Application)) + } + + // Ensure that we don't have any duplicate ports. +SKIPINSERT: + for _, newPort := range servicePorts { + // Skip to add new port. Instead, upsert port if it already exists. + for j, p := range base.Spec.Ports { + if strings.EqualFold(p.Name, newPort.Name) || p.Port == newPort.Port || p.TargetPort.IntVal == newPort.TargetPort.IntVal { + base.Spec.Ports[j] = newPort + continue SKIPINSERT + } + } + + // Add new port if it doesn't exist. + base.Spec.Ports = append(base.Spec.Ports, newPort) + } + + base.Spec.Selector = kubernetes.MakeSelectorLabels(appId.Name(), resource.Name) + base.Spec.Type = corev1.ServiceTypeClusterIP + + return rpv1.NewKubernetesOutputResource(rpv1.LocalIDService, base, base.ObjectMeta), nil +} + +func (r Renderer) makeDeployment( + ctx context.Context, + manifest kubeutil.ObjectManifest, + applicationName string, + options renderers.RenderOptions, + computedValues map[string]rpv1.ComputedValueReference, + resource *datamodel.ContainerResource, + roles []rpv1.OutputResource) ([]rpv1.OutputResource, map[string][]byte, error) { + // Keep track of the set of routes, we will need these to generate labels later + routes := []struct { + Name string + Type string + }{} + + // If the container requires azure role, it needs to configure workload identity (aka federated identity). + identityRequired := len(roles) > 0 + + dependencies := options.Dependencies + properties := resource.Properties + + normalizedName := kubernetes.NormalizeResourceName(resource.Name) + + deployment := getDeploymentBase(manifest, applicationName, resource, &options) + podSpec := &deployment.Spec.Template.Spec + + container := &podSpec.Containers[0] + for i, c := range podSpec.Containers { + if strings.EqualFold(c.Name, normalizedName) { + container = &podSpec.Containers[i] + break + } + } + + ports := []corev1.ContainerPort{} + for _, port := range properties.Container.Ports { + if provides := port.Provides; provides != "" { + resourceId, err := resources.ParseResource(provides) + if err != nil { + return []rpv1.OutputResource{}, nil, v1.NewClientErrInvalidRequest(err.Error()) + } + + routeName := kubernetes.NormalizeResourceName(resourceId.Name()) + routeType := resourceId.TypeSegments()[len(resourceId.TypeSegments())-1].Type + routeTypeParts := strings.Split(routeType, "/") + + routeTypeSuffix := kubernetes.NormalizeResourceName(routeTypeParts[len(routeTypeParts)-1]) + + routes = append(routes, struct { + Name string + Type string + }{Name: routeName, Type: routeTypeSuffix}) + + ports = append(ports, corev1.ContainerPort{ + // Name generation logic has to match the code in HttpRoute + Name: kubernetes.GetShortenedTargetPortName(routeTypeSuffix + routeName), + ContainerPort: port.ContainerPort, + Protocol: corev1.ProtocolTCP, + }) + } else { + ports = append(ports, corev1.ContainerPort{ + ContainerPort: port.ContainerPort, + Protocol: corev1.ProtocolTCP, + }) + } + } + + container.Image = properties.Container.Image + container.Ports = append(container.Ports, ports...) + container.Command = properties.Container.Command + container.Args = properties.Container.Args + container.WorkingDir = properties.Container.WorkingDir + + // If the user has specified an image pull policy, use it. Else, we will use Kubernetes default. + if properties.Container.ImagePullPolicy != "" { + container.ImagePullPolicy = corev1.PullPolicy(properties.Container.ImagePullPolicy) + } + + var err error + if !properties.Container.ReadinessProbe.IsEmpty() { + container.ReadinessProbe, err = r.makeHealthProbe(properties.Container.ReadinessProbe) + if err != nil { + return []rpv1.OutputResource{}, nil, fmt.Errorf("readiness probe encountered errors: %w ", err) + } + } + if !properties.Container.LivenessProbe.IsEmpty() { + container.LivenessProbe, err = r.makeHealthProbe(properties.Container.LivenessProbe) + if err != nil { + return []rpv1.OutputResource{}, nil, fmt.Errorf("liveness probe encountered errors: %w ", err) + } + } + + // We build the environment variable list in a stable order for testability + // For the values that come from connections we back them with secretData. We'll extract the values + // and return them. + env, secretData, err := getEnvVarsAndSecretData(resource, applicationName, dependencies) + if err != nil { + return []rpv1.OutputResource{}, nil, fmt.Errorf("failed to obtain environment variables and secret data: %w", err) + } + + for k, v := range properties.Container.Env { + env[k] = corev1.EnvVar{Name: k, Value: v} + } + + // Append in sorted order + for _, key := range getSortedKeys(env) { + container.Env = append(container.Env, env[key]) + } + + outputResources := []rpv1.OutputResource{} + deps := []string{} + + podLabels := kubernetes.MakeDescriptiveLabels(applicationName, resource.Name, resource.ResourceTypeName()) + + // Add volumes + volumes := []corev1.Volume{} + + // Create Kubernetes resource name scoped in Kubernetes namespace + kubeIdentityName := normalizedName + podSpec.ServiceAccountName = normalizedName + + // Create Azure resource name for managed/federated identity-scoped in resource group specified by Environment resource. + // To avoid the naming conflicts, we add the application name prefix to resource name. + azIdentityName := azrenderer.MakeResourceName(applicationName, resource.Name, azrenderer.Separator) + + for volumeName, volumeProperties := range properties.Container.Volumes { + // Based on the kind, create a persistent/ephemeral volume + switch volumeProperties.Kind { + case datamodel.Ephemeral: + volumeSpec, volumeMountSpec, err := makeEphemeralVolume(volumeName, volumeProperties.Ephemeral) + if err != nil { + return []rpv1.OutputResource{}, nil, fmt.Errorf("unable to create ephemeral volume spec for volume: %s - %w", volumeName, err) + } + // Add the volume mount to the Container spec + container.VolumeMounts = append(container.VolumeMounts, volumeMountSpec) + // Add the volume to the list of volumes to be added to the Volumes spec + volumes = append(volumes, volumeSpec) + case datamodel.Persistent: + var volumeSpec corev1.Volume + var volumeMountSpec corev1.VolumeMount + + properties, ok := dependencies[volumeProperties.Persistent.Source] + if !ok { + return []rpv1.OutputResource{}, nil, errors.New("volume dependency resource not found") + } + + vol, ok := properties.Resource.(*datamodel.VolumeResource) + if !ok { + return []rpv1.OutputResource{}, nil, errors.New("invalid dependency resource") + } + + switch vol.Properties.Kind { + case datamodel.AzureKeyVaultVolume: + // This will add the required managed identity resources. + identityRequired = true + + // Prepare role assignments + roleNames := []string{} + if len(vol.Properties.AzureKeyVault.Secrets) > 0 { + roleNames = append(roleNames, AzureKeyVaultSecretsUserRole) + } + if len(vol.Properties.AzureKeyVault.Certificates) > 0 || len(vol.Properties.AzureKeyVault.Keys) > 0 { + roleNames = append(roleNames, AzureKeyVaultCryptoUserRole) + } + + // Build RoleAssignment output.resource + kvID := vol.Properties.AzureKeyVault.Resource + roleAssignments, raDeps := azrenderer.MakeRoleAssignments(kvID, roleNames) + outputResources = append(outputResources, roleAssignments...) + deps = append(deps, raDeps...) + + // Create Per-Pod SecretProviderClass for the selected volume + // csiobjectspec must be generated when volume is updated. + objectSpec, err := handlers.GetMapValue[string](properties.ComputedValues, azvolrenderer.SPCVolumeObjectSpecKey) + if err != nil { + return []rpv1.OutputResource{}, nil, err + } + + spcName := kubernetes.NormalizeResourceName(vol.Name) + secretProvider, err := azrenderer.MakeKeyVaultSecretProviderClass(applicationName, spcName, vol, objectSpec, &options.Environment) + if err != nil { + return []rpv1.OutputResource{}, nil, err + } + outputResources = append(outputResources, *secretProvider) + deps = append(deps, rpv1.LocalIDSecretProviderClass) + + // Create volume spec which associated with secretProviderClass. + volumeSpec, volumeMountSpec, err = azrenderer.MakeKeyVaultVolumeSpec(volumeName, volumeProperties.Persistent.MountPath, spcName) + if err != nil { + return []rpv1.OutputResource{}, nil, fmt.Errorf("unable to create secretstore volume spec for volume: %s - %w", volumeName, err) + } + default: + return []rpv1.OutputResource{}, nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("Unsupported volume kind: %s for volume: %s. Supported kinds are: %v", vol.Properties.Kind, volumeName, GetSupportedKinds())) + } + + // Add the volume mount to the Container spec + container.VolumeMounts = append(container.VolumeMounts, volumeMountSpec) + // Add the volume to the list of volumes to be added to the Volumes spec + volumes = append(volumes, volumeSpec) + + // Add azurestorageaccountname and azurestorageaccountkey as secrets + // These will be added as key-value pairs to the kubernetes secret created for the container + // The key values are as per: https://docs.microsoft.com/en-us/azure/aks/azure-files-volume + for key, value := range properties.ComputedValues { + if value.(string) == rpv1.LocalIDAzureFileShareStorageAccount { + // The storage account was not created when the computed value was rendered + // Lookup the actual storage account name from the local id + id := properties.OutputResources[value.(string)] + value = id.Name() + } + secretData[key] = []byte(value.(string)) + } + default: + return []rpv1.OutputResource{}, secretData, v1.NewClientErrInvalidRequest(fmt.Sprintf("Only ephemeral or persistent volumes are supported. Got kind: %v", volumeProperties.Kind)) + } + } + + // In addition to the descriptive labels, we need to attach labels for each route + // so that the generated services can find these pods + for _, routeInfo := range routes { + routeLabels := kubernetes.MakeRouteSelectorLabels(applicationName, routeInfo.Type, routeInfo.Name) + podLabels = labels.Merge(routeLabels, podLabels) + } + + serviceAccountBase := getServiceAccountBase(manifest, applicationName, resource, &options) + // In order to enable per-container identity, it creates user-assigned managed identity, federated identity, and service account. + if identityRequired { + // 1. Create Per-Container managed identity. + managedIdentity, err := azrenderer.MakeManagedIdentity(azIdentityName, options.Environment.CloudProviders) + if err != nil { + return []rpv1.OutputResource{}, nil, err + } + outputResources = append(outputResources, *managedIdentity) + + // 2. Create Per-container federated identity resource. + fedIdentity, err := azrenderer.MakeFederatedIdentity(kubeIdentityName, &options.Environment) + if err != nil { + return []rpv1.OutputResource{}, nil, err + } + outputResources = append(outputResources, *fedIdentity) + + // 3. Create Per-container service account. + saAccount := azrenderer.SetWorkloadIdentityServiceAccount(serviceAccountBase) + outputResources = append(outputResources, *saAccount) + deps = append(deps, rpv1.LocalIDServiceAccount) + + // This is required to enable workload identity. + podLabels[azrenderer.AzureWorkloadIdentityUseKey] = "true" + + // 4. Add RBAC resources to the dependencies. + for _, role := range roles { + deps = append(deps, role.LocalID) + } + + computedValues[handlers.IdentityProperties] = rpv1.ComputedValueReference{ + Value: options.Environment.Identity, + Transformer: func(r v1.DataModelInterface, cv map[string]any) error { + ei, err := handlers.GetMapValue[*rpv1.IdentitySettings](cv, handlers.IdentityProperties) + if err != nil { + return err + } + res, ok := r.(*datamodel.ContainerResource) + if !ok { + return errors.New("resource must be ContainerResource") + } + if res.Properties.Identity == nil { + res.Properties.Identity = &rpv1.IdentitySettings{} + } + res.Properties.Identity.Kind = ei.Kind + res.Properties.Identity.OIDCIssuer = ei.OIDCIssuer + return nil + }, + } + + computedValues[handlers.UserAssignedIdentityIDKey] = rpv1.ComputedValueReference{ + LocalID: rpv1.LocalIDUserAssignedManagedIdentity, + PropertyReference: handlers.UserAssignedIdentityIDKey, + Transformer: func(r v1.DataModelInterface, cv map[string]any) error { + resourceID, err := handlers.GetMapValue[string](cv, handlers.UserAssignedIdentityIDKey) + if err != nil { + return err + } + res, ok := r.(*datamodel.ContainerResource) + if !ok { + return errors.New("resource must be ContainerResource") + } + if res.Properties.Identity == nil { + res.Properties.Identity = &rpv1.IdentitySettings{} + } + res.Properties.Identity.Resource = resourceID + return nil + }, + } + } else { + // If the container doesn't require identity, we'll use the default service account + or := rpv1.NewKubernetesOutputResource(rpv1.LocalIDServiceAccount, serviceAccountBase, serviceAccountBase.ObjectMeta) + outputResources = append(outputResources, or) + deps = append(deps, rpv1.LocalIDServiceAccount) + } + + // Create the role and role bindings for SA. + role := makeRBACRole(applicationName, kubeIdentityName, options.Environment.Namespace, resource) + outputResources = append(outputResources, *role) + deps = append(deps, rpv1.LocalIDKubernetesRole) + + roleBinding := makeRBACRoleBinding(applicationName, kubeIdentityName, podSpec.ServiceAccountName, options.Environment.Namespace, resource) + outputResources = append(outputResources, *roleBinding) + deps = append(deps, rpv1.LocalIDKubernetesRoleBinding) + + deployment.Spec.Template.ObjectMeta = mergeObjectMeta(deployment.Spec.Template.ObjectMeta, metav1.ObjectMeta{ + Labels: podLabels, + }) + + deployment.Spec.Selector = mergeLabelSelector(deployment.Spec.Selector, &metav1.LabelSelector{ + MatchLabels: kubernetes.MakeSelectorLabels(applicationName, resource.Name), + }) + + podSpec.Volumes = append(podSpec.Volumes, volumes...) + + // See: https://github.com/kubernetes/kubernetes/issues/92226 and + // https://github.com/radius-project/radius/issues/3002 + // + // Service links are a flawed and Kubernetes-only feature that we don't + // want to leak into Radius containers. + podSpec.EnableServiceLinks = to.Ptr(false) + + // If the user has specified a restart policy, use it. Else, it will use the Kubernetes default. + if properties.RestartPolicy != "" { + podSpec.RestartPolicy = corev1.RestartPolicy(properties.RestartPolicy) + } + + // If we have a secret to reference we need to ensure that the deployment will trigger a new revision + // when the secret changes. Normally referencing an environment variable from a secret will **NOT** cause + // a new revision when the secret changes. + // + // see: https://stackoverflow.com/questions/56711894/does-k8-update-environment-variables-when-secrets-change + // + // The solution to this is to embed the hash of the secret as an annotation in the deployment. This way when the + // secret changes we also change the content of the deployment and thus trigger a new revision. This is a very + // common solution to this problem, and not a bizarre workaround that we invented. + if len(secretData) > 0 { + hash := kubernetes.HashSecretData(secretData) + deployment.Spec.Template.ObjectMeta.Annotations[kubernetes.AnnotationSecretHash] = hash + deps = append(deps, rpv1.LocalIDSecret) + } + + // Patching Runtimes.Kubernetes.Pod to the PodSpec in deployment resource. + if properties.Runtimes != nil && properties.Runtimes.Kubernetes != nil && properties.Runtimes.Kubernetes.Pod != "" { + patchedPodSpec, err := patchPodSpec(podSpec, []byte(properties.Runtimes.Kubernetes.Pod)) + if err != nil { + return []rpv1.OutputResource{}, nil, fmt.Errorf("failed to patch PodSpec: %w", err) + } + deployment.Spec.Template.Spec = *patchedPodSpec + } + + deploymentOutput := rpv1.NewKubernetesOutputResource(rpv1.LocalIDDeployment, deployment, deployment.ObjectMeta) + deploymentOutput.CreateResource.Dependencies = deps + + outputResources = append(outputResources, deploymentOutput) + return outputResources, secretData, nil +} + +func getEnvVarsAndSecretData(resource *datamodel.ContainerResource, applicationName string, dependencies map[string]renderers.RendererDependency) (map[string]corev1.EnvVar, map[string][]byte, error) { + env := map[string]corev1.EnvVar{} + secretData := map[string][]byte{} + properties := resource.Properties + + // Take each connection and create environment variables for each part + // We'll store each value in a secret named with the same name as the resource. + // We'll use the environment variable names as keys. + // Float is used by the JSON serializer + for name, con := range properties.Connections { + properties := dependencies[con.Source] + if !con.GetDisableDefaultEnvVars() { + source := con.Source + if source == "" { + continue + } + + // handles case where container has source field structured as a URL. + if isURL(source) { + // parse source into scheme, hostname, and port. + scheme, hostname, port, err := parseURL(source) + if err != nil { + return map[string]corev1.EnvVar{}, map[string][]byte{}, fmt.Errorf("failed to parse source URL: %w", err) + } + + schemeKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "SCHEME") + hostnameKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "HOSTNAME") + portKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "PORT") + + env[schemeKey] = corev1.EnvVar{Name: schemeKey, Value: scheme} + env[hostnameKey] = corev1.EnvVar{Name: hostnameKey, Value: hostname} + env[portKey] = corev1.EnvVar{Name: portKey, Value: port} + + continue + } + + // handles case where container has source field structured as a resourceID. + for key, value := range properties.ComputedValues { + name := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), strings.ToUpper(key)) + + source := corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: kubernetes.NormalizeResourceName(resource.Name), + }, + Key: name, + }, + } + switch v := value.(type) { + case string: + secretData[name] = []byte(v) + env[name] = corev1.EnvVar{Name: name, ValueFrom: &source} + case float64: + secretData[name] = []byte(strconv.Itoa(int(v))) + env[name] = corev1.EnvVar{Name: name, ValueFrom: &source} + case int: + secretData[name] = []byte(strconv.Itoa(v)) + env[name] = corev1.EnvVar{Name: name, ValueFrom: &source} + } + } + } + } + + return env, secretData, nil +} + +func (r Renderer) makeHealthProbe(p datamodel.HealthProbeProperties) (*corev1.Probe, error) { + probeSpec := corev1.Probe{} + + switch p.Kind { + case datamodel.HTTPGetHealthProbe: + // Set the probe spec + probeSpec.ProbeHandler.HTTPGet = &corev1.HTTPGetAction{} + probeSpec.ProbeHandler.HTTPGet.Port = intstr.FromInt(int(p.HTTPGet.ContainerPort)) + probeSpec.ProbeHandler.HTTPGet.Path = p.HTTPGet.Path + httpHeaders := []corev1.HTTPHeader{} + for k, v := range p.HTTPGet.Headers { + httpHeaders = append(httpHeaders, corev1.HTTPHeader{ + Name: k, + Value: v, + }) + } + probeSpec.ProbeHandler.HTTPGet.HTTPHeaders = httpHeaders + c := containerHealthProbeConfig{ + initialDelaySeconds: p.HTTPGet.InitialDelaySeconds, + failureThreshold: p.HTTPGet.FailureThreshold, + periodSeconds: p.HTTPGet.PeriodSeconds, + timeoutSeconds: p.HTTPGet.TimeoutSeconds, + } + r.setContainerHealthProbeConfig(&probeSpec, c) + case datamodel.TCPHealthProbe: + // Set the probe spec + probeSpec.ProbeHandler.TCPSocket = &corev1.TCPSocketAction{} + probeSpec.TCPSocket.Port = intstr.FromInt(int(p.TCP.ContainerPort)) + c := containerHealthProbeConfig{ + initialDelaySeconds: p.TCP.InitialDelaySeconds, + failureThreshold: p.TCP.FailureThreshold, + periodSeconds: p.TCP.PeriodSeconds, + timeoutSeconds: p.TCP.TimeoutSeconds, + } + r.setContainerHealthProbeConfig(&probeSpec, c) + case datamodel.ExecHealthProbe: + // Set the probe spec + probeSpec.ProbeHandler.Exec = &corev1.ExecAction{} + probeSpec.Exec.Command = strings.Split(p.Exec.Command, " ") + c := containerHealthProbeConfig{ + initialDelaySeconds: p.Exec.InitialDelaySeconds, + failureThreshold: p.Exec.FailureThreshold, + periodSeconds: p.Exec.PeriodSeconds, + timeoutSeconds: p.Exec.TimeoutSeconds, + } + r.setContainerHealthProbeConfig(&probeSpec, c) + default: + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("health probe kind unsupported: %v", p.Kind)) + } + return &probeSpec, nil +} + +type containerHealthProbeConfig struct { + initialDelaySeconds *float32 + failureThreshold *float32 + periodSeconds *float32 + timeoutSeconds *float32 +} + +func (r Renderer) setContainerHealthProbeConfig(probeSpec *corev1.Probe, config containerHealthProbeConfig) { + // Initialize with Radius defaults and overwrite if values are specified + probeSpec.InitialDelaySeconds = DefaultInitialDelaySeconds + probeSpec.FailureThreshold = DefaultFailureThreshold + probeSpec.PeriodSeconds = DefaultPeriodSeconds + probeSpec.TimeoutSeconds = DefaultTimeoutSeconds + + if config.initialDelaySeconds != nil { + probeSpec.InitialDelaySeconds = int32(*config.initialDelaySeconds) + } + + if config.failureThreshold != nil { + probeSpec.FailureThreshold = int32(*config.failureThreshold) + } + + if config.periodSeconds != nil { + probeSpec.PeriodSeconds = int32(*config.periodSeconds) + } + + if config.timeoutSeconds != nil { + probeSpec.TimeoutSeconds = int32(*config.timeoutSeconds) + } +} + +func (r Renderer) makeSecret(ctx context.Context, resource datamodel.ContainerResource, applicationName string, secrets map[string][]byte, options renderers.RenderOptions) rpv1.OutputResource { + secret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: kubernetes.NormalizeResourceName(resource.Name), + Namespace: options.Environment.Namespace, + Labels: kubernetes.MakeDescriptiveLabels(applicationName, resource.Name, resource.ResourceTypeName()), + }, + Type: corev1.SecretTypeOpaque, + Data: secrets, + } + + output := rpv1.NewKubernetesOutputResource(rpv1.LocalIDSecret, &secret, secret.ObjectMeta) + return output +} + +func (r Renderer) isIdentitySupported(kind datamodel.IAMKind) bool { + if r.RoleAssignmentMap == nil || !kind.IsValid() { + return false + } + + _, ok := r.RoleAssignmentMap[kind] + return ok +} + +// Assigns roles/permissions to a specific resource for the managed identity resource. +func (r Renderer) makeRoleAssignmentsForResource(ctx context.Context, connection *datamodel.ConnectionProperties, dependencies map[string]renderers.RendererDependency) ([]rpv1.OutputResource, error) { + var roleNames []string + var armResourceIdentifier string + if connection.IAM.Kind.IsKind(datamodel.KindAzure) { + roleNames = append(roleNames, connection.IAM.Roles...) + armResourceIdentifier = connection.Source + } else { + // We're reporting errors in this code path to avoid obscuring a bug in another layer of the system. + // None of these error conditions should be caused by invalid user input. They should only be caused + // by internal bugs in Radius. + roleAssignmentData, ok := r.RoleAssignmentMap[connection.IAM.Kind] + if !ok { + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("RBAC is not supported for connection kind %q", connection.IAM.Kind)) + } + + // The dependency will have already been fetched by the system. + dependency, ok := dependencies[connection.Source] + if !ok { + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("connection source %q was not found in the dependencies collection", connection.Source)) + } + + // Find the matching output resource based on LocalID + target, ok := dependency.OutputResources[roleAssignmentData.LocalID] + if !ok { + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("output resource %q was not found in the outputs of dependency %q", roleAssignmentData.LocalID, connection.Source)) + } + + if !resources_azure.IsAzureResource(target) { + return nil, v1.NewClientErrInvalidRequest(fmt.Sprintf("output resource %q must be an ARM resource to support role assignments. Was: %+v", roleAssignmentData.LocalID, target)) + } + armResourceIdentifier = target.String() + + roleNames = roleAssignmentData.RoleNames + } + + outputResources := []rpv1.OutputResource{} + for _, roleName := range roleNames { + localID := rpv1.NewLocalID(rpv1.LocalIDRoleAssignmentPrefix, armResourceIdentifier, roleName) + roleAssignment := rpv1.OutputResource{ + + LocalID: localID, + CreateResource: &rpv1.Resource{ + Data: map[string]string{ + handlers.RoleNameKey: roleName, + handlers.RoleAssignmentScope: armResourceIdentifier, + }, + ResourceType: resourcemodel.ResourceType{ + Type: resources_azure.ResourceTypeAuthorizationRoleAssignment, + Provider: resourcemodel.ProviderAzure, + }, + Dependencies: []string{rpv1.LocalIDUserAssignedManagedIdentity}, + }, + } + + outputResources = append(outputResources, roleAssignment) + } + + return outputResources, nil +} + +func getSortedKeys(env map[string]corev1.EnvVar) []string { + keys := []string{} + for k := range env { + key := k + keys = append(keys, key) + } + + sort.Strings(keys) + return keys +} + +func isURL(input string) bool { + _, err := url.ParseRequestURI(input) + + // if first character is a slash, it's not a URL. It's a path. + if input == "" || err != nil || input[0] == '/' { + return false + } + return true +} + +func parseURL(sourceURL string) (scheme, hostname, port string, err error) { + u, err := url.Parse(sourceURL) + if err != nil { + return "", "", "", err + } + + scheme = u.Scheme + host := u.Host + + hostname, port, err = net.SplitHostPort(host) + if err != nil { + return "", "", "", err + } + + return scheme, hostname, port, nil +} diff --git a/pkg/platform-provider/k8sprovider/volumes.go b/pkg/platform-provider/k8sprovider/volumes.go new file mode 100644 index 0000000000..2c87fca113 --- /dev/null +++ b/pkg/platform-provider/k8sprovider/volumes.go @@ -0,0 +1,43 @@ +/* +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 k8sprovider + +import ( + "github.com/radius-project/radius/pkg/corerp/datamodel" + + corev1 "k8s.io/api/core/v1" +) + +// Create the volume specs for Pod. +func makeEphemeralVolume(volumeName string, volume *datamodel.EphemeralVolume) (corev1.Volume, corev1.VolumeMount, error) { + // Make volume spec + volumeSpec := corev1.Volume{} + volumeSpec.Name = volumeName + volumeSpec.VolumeSource.EmptyDir = &corev1.EmptyDirVolumeSource{} + if volume != nil && volume.ManagedStore == datamodel.ManagedStoreMemory { + volumeSpec.VolumeSource.EmptyDir.Medium = corev1.StorageMediumMemory + } else { + volumeSpec.VolumeSource.EmptyDir.Medium = corev1.StorageMediumDefault + } + + // Make volumeMount spec + volumeMountSpec := corev1.VolumeMount{} + volumeMountSpec.MountPath = volume.MountPath + volumeMountSpec.Name = volumeName + + return volumeSpec, volumeMountSpec, nil +} diff --git a/pkg/platform-provider/types.go b/pkg/platform-provider/types.go new file mode 100644 index 0000000000..d10e37d72a --- /dev/null +++ b/pkg/platform-provider/types.go @@ -0,0 +1,60 @@ +package platformprovider + +import ( + "context" + + "github.com/radius-project/radius/pkg/corerp/datamodel" + "github.com/radius-project/radius/pkg/ucp/resources" +) + +// Provider is the interface that must be implemented by a platform provider. +type Provider interface { + Initialize() error + + // Name returns the name of the platform provider. + Name() string + + // Container returns the container interface. Container represents container orchestration. + Container() (ContainerProvider, error) + + // Route returns the route interface. + Route() (RouteProvider, error) + + // Gateway returns the gateway interface. + Gateway() (GatewayProvider, error) + + // Identity returns the identity interface. + Identity() (IdentityProvider, error) + + // Volume returns the volume interface. + Volume() (VolumeProvider, error) + + // SecretStore returns the secret store interface. + SecretStore() (SecretStoreProvider, error) +} + +type ContainerProvider interface { + CreateOrUpdateContainer(ctx context.Context, container *datamodel.ContainerResource) error +} + +type RouteProvider interface { + CreateOrUpdateRoute(ctx context.Context) error + DeleteRoute(ctx context.Context) error +} + +type GatewayProvider interface { + CreateOrUpdateGateway(ctx context.Context, gateway *datamodel.Gateway) error +} + +type IdentityProvider interface { + CreateOrUpdateIdentity(ctx context.Context) (*resources.ID, error) + AssignRoleToIdentity(ctx context.Context) error +} + +type VolumeProvider interface { +} + +type SecretStoreProvider interface { + CreateOrUpdateSecretStore(ctx context.Context, secretStore *datamodel.SecretStore) error + DeleteSecretStore(ctx context.Context) error +} From 3d62689aca522209b939a98acc385fede099c0a4 Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Thu, 21 Mar 2024 11:52:23 -0700 Subject: [PATCH 2/2] wip Signed-off-by: Young Bu Park --- .../backend/containers/createorupdate.go | 1 + pkg/platform-provider/factory.go | 64 +++++++++++++++++++ pkg/platform-provider/k8sprovider/provider.go | 36 +++++++++-- pkg/platform-provider/types.go | 28 ++++++-- 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 pkg/platform-provider/factory.go diff --git a/pkg/corerp/backend/containers/createorupdate.go b/pkg/corerp/backend/containers/createorupdate.go index 8da55c05f6..156956d54b 100644 --- a/pkg/corerp/backend/containers/createorupdate.go +++ b/pkg/corerp/backend/containers/createorupdate.go @@ -336,6 +336,7 @@ func (c *CreateOrUpdateResource) Run(ctx context.Context, request *ctrl.Request) envVars, err := getEnvironmentVariables(container, connectedResourceMap) // Get volumes and if Azure Keyvault volume is found, add AzureKeyVaultSecretsUserRole, AzureKeyVaultCryptoUserRole to "roles". + // Note: Portable resource can have a custom action api to provide Supported roles. for volName, volProperties := range properties.Container.Volumes { if volProperties.Kind == datamodel.Persistent { volumeResource, ok := connectedResourceMap[volProperties.Persistent.Source] diff --git a/pkg/platform-provider/factory.go b/pkg/platform-provider/factory.go new file mode 100644 index 0000000000..659c483ee8 --- /dev/null +++ b/pkg/platform-provider/factory.go @@ -0,0 +1,64 @@ +/* +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 platformprovider + +import "sync" + +// Factory is a function that returns a new platform provider. +type Factory func() (Provider, error) + +var ( + registryMu sync.Mutex + providerRegistry = make(map[string]Factory) +) + +// Register registers a platformprovider.Factory by name. +func Register(name string, platform Factory) { + registryMu.Lock() + defer registryMu.Unlock() + + if platform == nil { + panic("platform-provider: Register platform is nil") + } + + if _, dup := providerRegistry[name]; dup { + panic("platform-provider: Register called twice for platform " + name) + } + providerRegistry[name] = platform +} + +// GetPlatform creates an instance of the named platform provider, or nil if the name is not registered. +func GetPlatform(name string) (Provider, error) { + registryMu.Lock() + defer registryMu.Unlock() + + platform, ok := providerRegistry[name] + if !ok { + return nil, nil + } + + return platform() +} + +// NewProvider creates an instance of the named platform provider, or nil if the name is not registered. +func NewProvider(name string) (Provider, error) { + if name == "" { + return nil, nil + } + + return GetPlatform(name) +} diff --git a/pkg/platform-provider/k8sprovider/provider.go b/pkg/platform-provider/k8sprovider/provider.go index 679d22dec1..68533563af 100644 --- a/pkg/platform-provider/k8sprovider/provider.go +++ b/pkg/platform-provider/k8sprovider/provider.go @@ -1,3 +1,19 @@ +/* +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 k8sprovider import ( @@ -7,6 +23,12 @@ import ( platformprovider "github.com/radius-project/radius/pkg/platform-provider" ) +func init() { + platformprovider.Register("kubernetes", func() (platformprovider.Provider, error) { + return &KubeProvider{}, nil + }) +} + var _ platformprovider.Provider = (*KubeProvider)(nil) var _ platformprovider.ContainerProvider = (*KubeProvider)(nil) @@ -18,30 +40,30 @@ func (p *KubeProvider) Initialize() error { } func (p *KubeProvider) Name() string { - return "k8s" + return "kubernetes" } -func (p *KubeProvider) Container() (platformprovider.ContainerProvider, error) { +func (p *KubeProvider) Container(name string) (platformprovider.ContainerProvider, error) { return nil, nil } -func (p *KubeProvider) Route() (platformprovider.RouteProvider, error) { +func (p *KubeProvider) Route(name string) (platformprovider.RouteProvider, error) { return nil, nil } -func (p *KubeProvider) Gateway() (platformprovider.GatewayProvider, error) { +func (p *KubeProvider) Gateway(name string) (platformprovider.GatewayProvider, error) { return nil, nil } -func (p *KubeProvider) Identity() (platformprovider.IdentityProvider, error) { +func (p *KubeProvider) Identity(name string) (platformprovider.IdentityProvider, error) { return nil, nil } -func (p *KubeProvider) Volume() (platformprovider.VolumeProvider, error) { +func (p *KubeProvider) Volume(name string) (platformprovider.VolumeProvider, error) { return nil, nil } -func (p *KubeProvider) SecretStore() (platformprovider.SecretStoreProvider, error) { +func (p *KubeProvider) SecretStore(name string) (platformprovider.SecretStoreProvider, error) { return nil, nil } diff --git a/pkg/platform-provider/types.go b/pkg/platform-provider/types.go index d10e37d72a..795f43dab0 100644 --- a/pkg/platform-provider/types.go +++ b/pkg/platform-provider/types.go @@ -1,3 +1,19 @@ +/* +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 platformprovider import ( @@ -15,22 +31,22 @@ type Provider interface { Name() string // Container returns the container interface. Container represents container orchestration. - Container() (ContainerProvider, error) + Container(name string) (ContainerProvider, error) // Route returns the route interface. - Route() (RouteProvider, error) + Route(name string) (RouteProvider, error) // Gateway returns the gateway interface. - Gateway() (GatewayProvider, error) + Gateway(name string) (GatewayProvider, error) // Identity returns the identity interface. - Identity() (IdentityProvider, error) + Identity(name string) (IdentityProvider, error) // Volume returns the volume interface. - Volume() (VolumeProvider, error) + Volume(name string) (VolumeProvider, error) // SecretStore returns the secret store interface. - SecretStore() (SecretStoreProvider, error) + SecretStore(name string) (SecretStoreProvider, error) } type ContainerProvider interface {