diff --git a/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json b/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json index 378210d4ef..21b07689de 100644 --- a/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json +++ b/hack/bicep-types-radius/generated/applications/applications.core/2023-10-01-preview/types.json @@ -1 +1 @@ -[{"1":{"Kind":1}},{"1":{"Kind":2}},{"1":{"Kind":3}},{"1":{"Kind":4}},{"1":{"Kind":5}},{"1":{"Kind":6}},{"1":{"Kind":7}},{"1":{"Kind":8}},{"6":{"Value":"Applications.Core/applications"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/applications","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":8,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":9,"Flags":10,"Description":"The resource api version"},"properties":{"Type":11,"Flags":1,"Description":"Application properties"},"tags":{"Type":46,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"ApplicationProperties","Properties":{"provisioningState":{"Type":19,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"environment":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"extensions":{"Type":34,"Flags":0,"Description":"The application extension."},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[12,13,14,15,16,17,18]}},{"7":{"Name":"Extension","Discriminator":"kind","BaseProperties":{},"Elements":{"daprSidecar":21,"kubernetesMetadata":26,"kubernetesNamespace":30,"manualScaling":32}}},{"2":{"Name":"DaprSidecarExtension","Properties":{"appPort":{"Type":3,"Flags":0,"Description":"The Dapr appPort. Specifies the internal listening port for the application to handle requests from the Dapr sidecar."},"appId":{"Type":4,"Flags":1,"Description":"The Dapr appId. Specifies the identifier used by Dapr for service invocation."},"config":{"Type":4,"Flags":0,"Description":"Specifies the Dapr configuration to use for the resource."},"protocol":{"Type":24,"Flags":0,"Description":"The Dapr sidecar extension protocol"},"kind":{"Type":25,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"6":{"Value":"http"}},{"6":{"Value":"grpc"}},{"5":{"Elements":[22,23]}},{"6":{"Value":"daprSidecar"}},{"2":{"Name":"KubernetesMetadataExtension","Properties":{"annotations":{"Type":27,"Flags":0,"Description":"Annotations to be applied to the Kubernetes resources output by the resource"},"labels":{"Type":28,"Flags":0,"Description":"Labels to be applied to the Kubernetes resources output by the resource"},"kind":{"Type":29,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"2":{"Name":"KubernetesMetadataExtensionAnnotations","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"KubernetesMetadataExtensionLabels","Properties":{},"AdditionalProperties":4}},{"6":{"Value":"kubernetesMetadata"}},{"2":{"Name":"KubernetesNamespaceExtension","Properties":{"namespace":{"Type":4,"Flags":1,"Description":"The namespace of the application environment."},"kind":{"Type":31,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"6":{"Value":"kubernetesNamespace"}},{"2":{"Name":"ManualScalingExtension","Properties":{"replicas":{"Type":3,"Flags":1,"Description":"Replica count."},"kind":{"Type":33,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"6":{"Value":"manualScaling"}},{"3":{"ItemType":20}},{"2":{"Name":"ResourceStatus","Properties":{"compute":{"Type":36,"Flags":0,"Description":"Represents backing compute resource"},"recipe":{"Type":43,"Flags":2,"Description":"Recipe status at deployment time for a resource."},"outputResources":{"Type":45,"Flags":0,"Description":"Properties of an output resource"}}}},{"7":{"Name":"EnvironmentCompute","Discriminator":"kind","BaseProperties":{"resourceId":{"Type":4,"Flags":0,"Description":"The resource id of the compute resource for application environment."},"identity":{"Type":37,"Flags":0,"Description":"IdentitySettings is the external identity setting."}},"Elements":{"kubernetes":41}}},{"2":{"Name":"IdentitySettings","Properties":{"kind":{"Type":40,"Flags":1,"Description":"IdentitySettingKind is the kind of supported external identity setting"},"oidcIssuer":{"Type":4,"Flags":0,"Description":"The URI for your compute platform's OIDC issuer"},"resource":{"Type":4,"Flags":0,"Description":"The resource ID of the provisioned identity"}}}},{"6":{"Value":"undefined"}},{"6":{"Value":"azure.com.workload"}},{"5":{"Elements":[38,39]}},{"2":{"Name":"KubernetesCompute","Properties":{"namespace":{"Type":4,"Flags":1,"Description":"The namespace to use for the environment."},"kind":{"Type":42,"Flags":1,"Description":"Discriminator property for EnvironmentCompute."}}}},{"6":{"Value":"kubernetes"}},{"2":{"Name":"RecipeStatus","Properties":{"templateKind":{"Type":4,"Flags":1,"Description":"TemplateKind is the kind of the recipe template used by the portable resource upon deployment."},"templatePath":{"Type":4,"Flags":1,"Description":"TemplatePath is the path of the recipe consumed by the portable resource upon deployment."},"templateVersion":{"Type":4,"Flags":0,"Description":"TemplateVersion is the version number of the template."}}}},{"2":{"Name":"OutputResource","Properties":{"localId":{"Type":4,"Flags":0,"Description":"The logical identifier scoped to the owning Radius resource. This is only needed or used when a resource has a dependency relationship. LocalIDs do not have any particular format or meaning beyond being compared to determine dependency relationships."},"id":{"Type":4,"Flags":0,"Description":"The UCP resource ID of the underlying resource."},"radiusManaged":{"Type":2,"Flags":0,"Description":"Determines whether Radius manages the lifecycle of the underlying resource."}}}},{"3":{"ItemType":44}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"SystemData","Properties":{"createdBy":{"Type":4,"Flags":0,"Description":"The identity that created the resource."},"createdByType":{"Type":52,"Flags":0,"Description":"The type of identity that created the resource."},"createdAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource creation (UTC)."},"lastModifiedBy":{"Type":4,"Flags":0,"Description":"The identity that last modified the resource."},"lastModifiedByType":{"Type":57,"Flags":0,"Description":"The type of identity that created the resource."},"lastModifiedAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource last modification (UTC)"}}}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[48,49,50,51]}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[53,54,55,56]}},{"4":{"Name":"Applications.Core/applications@2023-10-01-preview","ScopeType":0,"Body":10}},{"6":{"Value":"Applications.Core/containers"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/containers","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":59,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":60,"Flags":10,"Description":"The resource api version"},"properties":{"Type":62,"Flags":1,"Description":"Container properties"},"tags":{"Type":122,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"ContainerProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":70,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"container":{"Type":71,"Flags":1,"Description":"Definition of a container"},"connections":{"Type":108,"Flags":0,"Description":"Specifies a connection to another resource."},"identity":{"Type":37,"Flags":0,"Description":"IdentitySettings is the external identity setting."},"extensions":{"Type":109,"Flags":0,"Description":"Extensions spec of the resource"},"resourceProvisioning":{"Type":112,"Flags":0,"Description":"Specifies how the underlying service/resource is provisioned and managed. Available values are 'internal', where Radius manages the lifecycle of the resource internally, and 'manual', where a user manages the resource."},"resources":{"Type":114,"Flags":0,"Description":"A collection of references to resources associated with the container"},"restartPolicy":{"Type":118,"Flags":0,"Description":"Restart policy for the container"},"runtimes":{"Type":119,"Flags":0,"Description":"The properties for runtime configuration"}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[63,64,65,66,67,68,69]}},{"2":{"Name":"Container","Properties":{"image":{"Type":4,"Flags":1,"Description":"The registry and image to download and run in your container"},"imagePullPolicy":{"Type":75,"Flags":0,"Description":"The image pull policy for the container"},"env":{"Type":76,"Flags":0,"Description":"environment"},"ports":{"Type":81,"Flags":0,"Description":"container ports"},"readinessProbe":{"Type":82,"Flags":0,"Description":"Properties for readiness/liveness probe"},"livenessProbe":{"Type":82,"Flags":0,"Description":"Properties for readiness/liveness probe"},"volumes":{"Type":101,"Flags":0,"Description":"container volumes"},"command":{"Type":102,"Flags":0,"Description":"Entrypoint array. Overrides the container image's ENTRYPOINT"},"args":{"Type":103,"Flags":0,"Description":"Arguments to the entrypoint. Overrides the container image's CMD"},"workingDir":{"Type":4,"Flags":0,"Description":"Working directory for the container"}}}},{"6":{"Value":"Always"}},{"6":{"Value":"IfNotPresent"}},{"6":{"Value":"Never"}},{"5":{"Elements":[72,73,74]}},{"2":{"Name":"ContainerEnv","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"ContainerPortProperties","Properties":{"containerPort":{"Type":3,"Flags":1,"Description":"The listening port number"},"protocol":{"Type":80,"Flags":0,"Description":"The protocol in use by the port"},"provides":{"Type":4,"Flags":0,"Description":"Specifies a route provided by this port"},"scheme":{"Type":4,"Flags":0,"Description":"Specifies the URL scheme of the communication protocol. Consumers can use the scheme to construct a URL. The value defaults to 'http' or 'https' depending on the port value"},"port":{"Type":3,"Flags":0,"Description":"Specifies the port that will be exposed by this container. Must be set when value different from containerPort is desired"}}}},{"6":{"Value":"TCP"}},{"6":{"Value":"UDP"}},{"5":{"Elements":[78,79]}},{"2":{"Name":"ContainerPorts","Properties":{},"AdditionalProperties":77}},{"7":{"Name":"HealthProbeProperties","Discriminator":"kind","BaseProperties":{"initialDelaySeconds":{"Type":3,"Flags":0,"Description":"Initial delay in seconds before probing for readiness/liveness"},"failureThreshold":{"Type":3,"Flags":0,"Description":"Threshold number of times the probe fails after which a failure would be reported"},"periodSeconds":{"Type":3,"Flags":0,"Description":"Interval for the readiness/liveness probe in seconds"},"timeoutSeconds":{"Type":3,"Flags":0,"Description":"Number of seconds after which the readiness/liveness probe times out. Defaults to 5 seconds"}},"Elements":{"exec":83,"httpGet":85,"tcp":88}}},{"2":{"Name":"ExecHealthProbeProperties","Properties":{"command":{"Type":4,"Flags":1,"Description":"Command to execute to probe readiness/liveness"},"kind":{"Type":84,"Flags":1,"Description":"Discriminator property for HealthProbeProperties."}}}},{"6":{"Value":"exec"}},{"2":{"Name":"HttpGetHealthProbeProperties","Properties":{"containerPort":{"Type":3,"Flags":1,"Description":"The listening port number"},"path":{"Type":4,"Flags":1,"Description":"The route to make the HTTP request on"},"headers":{"Type":86,"Flags":0,"Description":"Custom HTTP headers to add to the get request"},"kind":{"Type":87,"Flags":1,"Description":"Discriminator property for HealthProbeProperties."}}}},{"2":{"Name":"HttpGetHealthProbePropertiesHeaders","Properties":{},"AdditionalProperties":4}},{"6":{"Value":"httpGet"}},{"2":{"Name":"TcpHealthProbeProperties","Properties":{"containerPort":{"Type":3,"Flags":1,"Description":"The listening port number"},"kind":{"Type":89,"Flags":1,"Description":"Discriminator property for HealthProbeProperties."}}}},{"6":{"Value":"tcp"}},{"7":{"Name":"Volume","Discriminator":"kind","BaseProperties":{"mountPath":{"Type":4,"Flags":0,"Description":"The path where the volume is mounted"}},"Elements":{"ephemeral":91,"persistent":96}}},{"2":{"Name":"EphemeralVolume","Properties":{"managedStore":{"Type":94,"Flags":1,"Description":"The managed store for the ephemeral volume"},"kind":{"Type":95,"Flags":1,"Description":"Discriminator property for Volume."}}}},{"6":{"Value":"memory"}},{"6":{"Value":"disk"}},{"5":{"Elements":[92,93]}},{"6":{"Value":"ephemeral"}},{"2":{"Name":"PersistentVolume","Properties":{"permission":{"Type":99,"Flags":0,"Description":"The persistent volume permission"},"source":{"Type":4,"Flags":1,"Description":"The source of the volume"},"kind":{"Type":100,"Flags":1,"Description":"Discriminator property for Volume."}}}},{"6":{"Value":"read"}},{"6":{"Value":"write"}},{"5":{"Elements":[97,98]}},{"6":{"Value":"persistent"}},{"2":{"Name":"ContainerVolumes","Properties":{},"AdditionalProperties":90}},{"3":{"ItemType":4}},{"3":{"ItemType":4}},{"2":{"Name":"ConnectionProperties","Properties":{"source":{"Type":4,"Flags":1,"Description":"The source of the connection"},"disableDefaultEnvVars":{"Type":2,"Flags":0,"Description":"default environment variable override"},"iam":{"Type":105,"Flags":0,"Description":"IAM properties"}}}},{"2":{"Name":"IamProperties","Properties":{"kind":{"Type":106,"Flags":1,"Description":"The kind of IAM provider to configure"},"roles":{"Type":107,"Flags":0,"Description":"RBAC permissions to be assigned on the source resource"}}}},{"6":{"Value":"azure"}},{"3":{"ItemType":4}},{"2":{"Name":"ContainerPropertiesConnections","Properties":{},"AdditionalProperties":104}},{"3":{"ItemType":20}},{"6":{"Value":"internal"}},{"6":{"Value":"manual"}},{"5":{"Elements":[110,111]}},{"2":{"Name":"ResourceReference","Properties":{"id":{"Type":4,"Flags":1,"Description":"Resource id of an existing resource"}}}},{"3":{"ItemType":113}},{"6":{"Value":"Always"}},{"6":{"Value":"OnFailure"}},{"6":{"Value":"Never"}},{"5":{"Elements":[115,116,117]}},{"2":{"Name":"RuntimesProperties","Properties":{"kubernetes":{"Type":120,"Flags":0,"Description":"The runtime configuration properties for Kubernetes"}}}},{"2":{"Name":"KubernetesRuntimeProperties","Properties":{"base":{"Type":4,"Flags":0,"Description":"The serialized YAML manifest which represents the base Kubernetes resources to deploy, such as Deployment, Service, ServiceAccount, Secrets, and ConfigMaps."},"pod":{"Type":121,"Flags":0,"Description":"A strategic merge patch that will be applied to the PodSpec object when this container is being deployed."}}}},{"2":{"Name":"KubernetesPodSpec","Properties":{},"AdditionalProperties":0}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/containers@2023-10-01-preview","ScopeType":0,"Body":61}},{"6":{"Value":"Applications.Core/environments"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/environments","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":124,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":125,"Flags":10,"Description":"The resource api version"},"properties":{"Type":127,"Flags":1,"Description":"Environment properties"},"tags":{"Type":147,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"EnvironmentProperties","Properties":{"provisioningState":{"Type":135,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"compute":{"Type":36,"Flags":1,"Description":"Represents backing compute resource"},"providers":{"Type":136,"Flags":0,"Description":"The Cloud providers configuration"},"simulated":{"Type":2,"Flags":0,"Description":"Simulated environment."},"recipes":{"Type":145,"Flags":0,"Description":"Specifies Recipes linked to the Environment."},"extensions":{"Type":146,"Flags":0,"Description":"The environment extension."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[128,129,130,131,132,133,134]}},{"2":{"Name":"Providers","Properties":{"azure":{"Type":137,"Flags":0,"Description":"The Azure cloud provider definition"},"aws":{"Type":138,"Flags":0,"Description":"The AWS cloud provider definition"}}}},{"2":{"Name":"ProvidersAzure","Properties":{"scope":{"Type":4,"Flags":1,"Description":"Target scope for Azure resources to be deployed into. For example: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup'"}}}},{"2":{"Name":"ProvidersAws","Properties":{"scope":{"Type":4,"Flags":1,"Description":"Target scope for AWS resources to be deployed into. For example: '/planes/aws/aws/accounts/000000000000/regions/us-west-2'"}}}},{"7":{"Name":"RecipeProperties","Discriminator":"templateKind","BaseProperties":{"templatePath":{"Type":4,"Flags":1,"Description":"Path to the template provided by the recipe. Currently only link to Azure Container Registry is supported."},"parameters":{"Type":0,"Flags":0,"Description":"Any object"}},"Elements":{"bicep":140,"terraform":142}}},{"2":{"Name":"BicepRecipeProperties","Properties":{"plainHttp":{"Type":2,"Flags":0,"Description":"Connect to the Bicep registry using HTTP (not-HTTPS). This should be used when the registry is known not to support HTTPS, for example in a locally-hosted registry. Defaults to false (use HTTPS/TLS)."},"templateKind":{"Type":141,"Flags":1,"Description":"Discriminator property for RecipeProperties."}}}},{"6":{"Value":"bicep"}},{"2":{"Name":"TerraformRecipeProperties","Properties":{"templateVersion":{"Type":4,"Flags":0,"Description":"Version of the template to deploy. For Terraform recipes using a module registry this is required, but must be omitted for other module sources."},"templateKind":{"Type":143,"Flags":1,"Description":"Discriminator property for RecipeProperties."}}}},{"6":{"Value":"terraform"}},{"2":{"Name":"DictionaryOfRecipeProperties","Properties":{},"AdditionalProperties":139}},{"2":{"Name":"EnvironmentPropertiesRecipes","Properties":{},"AdditionalProperties":144}},{"3":{"ItemType":20}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/environments@2023-10-01-preview","ScopeType":0,"Body":126}},{"6":{"Value":"Applications.Core/extenders"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/extenders","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":149,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":150,"Flags":10,"Description":"The resource api version"},"properties":{"Type":152,"Flags":1,"Description":"ExtenderResource portable resource properties"},"tags":{"Type":165,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"ExtenderProperties","Properties":{"environment":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the environment that the portable resource is linked to"},"application":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the application that the portable resource is consumed by (if applicable)"},"provisioningState":{"Type":160,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"secrets":{"Type":0,"Flags":0,"Description":"Any object"},"recipe":{"Type":161,"Flags":0,"Description":"The recipe used to automatically deploy underlying infrastructure for a portable resource"},"resourceProvisioning":{"Type":164,"Flags":0,"Description":"Specifies how the underlying service/resource is provisioned and managed. Available values are 'recipe', where Radius manages the lifecycle of the resource through a Recipe, and 'manual', where a user manages the resource and provides the values."}},"AdditionalProperties":0}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[153,154,155,156,157,158,159]}},{"2":{"Name":"Recipe","Properties":{"name":{"Type":4,"Flags":1,"Description":"The name of the recipe within the environment to use"},"parameters":{"Type":0,"Flags":0,"Description":"Any object"}}}},{"6":{"Value":"recipe"}},{"6":{"Value":"manual"}},{"5":{"Elements":[162,163]}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/extenders@2023-10-01-preview","ScopeType":0,"Body":151}},{"6":{"Value":"Applications.Core/gateways"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/gateways","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":167,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":168,"Flags":10,"Description":"The resource api version"},"properties":{"Type":170,"Flags":1,"Description":"Gateway properties"},"tags":{"Type":186,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"GatewayProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":178,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"internal":{"Type":2,"Flags":0,"Description":"Sets Gateway to not be exposed externally (no public IP address associated). Defaults to false (exposed to internet)."},"hostname":{"Type":179,"Flags":0,"Description":"Declare hostname information for the Gateway. Leaving the hostname empty auto-assigns one: mygateway.myapp.PUBLICHOSTNAMEORIP.nip.io."},"routes":{"Type":181,"Flags":1,"Description":"Routes attached to this Gateway"},"tls":{"Type":182,"Flags":0,"Description":"TLS configuration definition for Gateway resource."},"url":{"Type":4,"Flags":2,"Description":"URL of the gateway resource. Readonly"}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[171,172,173,174,175,176,177]}},{"2":{"Name":"GatewayHostname","Properties":{"prefix":{"Type":4,"Flags":0,"Description":"Specify a prefix for the hostname: myhostname.myapp.PUBLICHOSTNAMEORIP.nip.io. Mutually exclusive with 'fullyQualifiedHostname' and will be overridden if both are defined."},"fullyQualifiedHostname":{"Type":4,"Flags":0,"Description":"Specify a fully-qualified domain name: myapp.mydomain.com. Mutually exclusive with 'prefix' and will take priority if both are defined."}}}},{"2":{"Name":"GatewayRoute","Properties":{"path":{"Type":4,"Flags":0,"Description":"The path to match the incoming request path on. Ex - /myservice."},"destination":{"Type":4,"Flags":0,"Description":"The HttpRoute to route to. Ex - myserviceroute.id."},"replacePrefix":{"Type":4,"Flags":0,"Description":"Optionally update the prefix when sending the request to the service. Ex - replacePrefix: '/' and path: '/myservice' will transform '/myservice/myroute' to '/myroute'"}}}},{"3":{"ItemType":180}},{"2":{"Name":"GatewayTls","Properties":{"sslPassthrough":{"Type":2,"Flags":0,"Description":"If true, gateway lets the https traffic sslPassthrough to the backend servers for decryption."},"minimumProtocolVersion":{"Type":185,"Flags":0,"Description":"Tls Minimum versions for Gateway resource."},"certificateFrom":{"Type":4,"Flags":0,"Description":"The resource id for the secret containing the TLS certificate and key for the gateway."}}}},{"6":{"Value":"1.2"}},{"6":{"Value":"1.3"}},{"5":{"Elements":[183,184]}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/gateways@2023-10-01-preview","ScopeType":0,"Body":169}},{"6":{"Value":"Applications.Core/httpRoutes"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/httpRoutes","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":188,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":189,"Flags":10,"Description":"The resource api version"},"properties":{"Type":191,"Flags":1,"Description":"HTTPRoute properties"},"tags":{"Type":200,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"HttpRouteProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":199,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"hostname":{"Type":4,"Flags":0,"Description":"The internal hostname accepting traffic for the HTTP Route. Readonly."},"port":{"Type":3,"Flags":0,"Description":"The port number for the HTTP Route. Defaults to 80. Readonly."},"scheme":{"Type":4,"Flags":2,"Description":"The scheme used for traffic. Readonly."},"url":{"Type":4,"Flags":2,"Description":"A stable URL that that can be used to route traffic to a resource. Readonly."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[192,193,194,195,196,197,198]}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/httpRoutes@2023-10-01-preview","ScopeType":0,"Body":190}},{"6":{"Value":"Applications.Core/secretStores"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/secretStores","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":202,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":203,"Flags":10,"Description":"The resource api version"},"properties":{"Type":205,"Flags":1,"Description":"The properties of SecretStore"},"tags":{"Type":223,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"SecretStoreProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":213,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"type":{"Type":216,"Flags":0,"Description":"The type of SecretStore data"},"data":{"Type":222,"Flags":1,"Description":"An object to represent key-value type secrets"},"resource":{"Type":4,"Flags":0,"Description":"The resource id of external secret store."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[206,207,208,209,210,211,212]}},{"6":{"Value":"generic"}},{"6":{"Value":"certificate"}},{"5":{"Elements":[214,215]}},{"2":{"Name":"SecretValueProperties","Properties":{"encoding":{"Type":220,"Flags":0,"Description":"The type of SecretValue Encoding"},"value":{"Type":4,"Flags":0,"Description":"The value of secret."},"valueFrom":{"Type":221,"Flags":0,"Description":"The Secret value source properties"}}}},{"6":{"Value":"raw"}},{"6":{"Value":"base64"}},{"5":{"Elements":[218,219]}},{"2":{"Name":"ValueFromProperties","Properties":{"name":{"Type":4,"Flags":1,"Description":"The name of the referenced secret."},"version":{"Type":4,"Flags":0,"Description":"The version of the referenced secret."}}}},{"2":{"Name":"SecretStorePropertiesData","Properties":{},"AdditionalProperties":217}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/secretStores@2023-10-01-preview","ScopeType":0,"Body":204}},{"6":{"Value":"Applications.Core/volumes"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/volumes","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":225,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":226,"Flags":10,"Description":"The resource api version"},"properties":{"Type":228,"Flags":1,"Description":"Volume properties"},"tags":{"Type":260,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"7":{"Name":"VolumeProperties","Discriminator":"kind","BaseProperties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":236,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."}},"Elements":{"azure.com.keyvault":237}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[229,230,231,232,233,234,235]}},{"2":{"Name":"AzureKeyVaultVolumeProperties","Properties":{"certificates":{"Type":250,"Flags":0,"Description":"The KeyVault certificates that this volume exposes"},"keys":{"Type":252,"Flags":0,"Description":"The KeyVault keys that this volume exposes"},"resource":{"Type":4,"Flags":1,"Description":"The ID of the keyvault to use for this volume resource"},"secrets":{"Type":258,"Flags":0,"Description":"The KeyVault secrets that this volume exposes"},"kind":{"Type":259,"Flags":1,"Description":"Discriminator property for VolumeProperties."}}}},{"2":{"Name":"CertificateObjectProperties","Properties":{"alias":{"Type":4,"Flags":0,"Description":"File name when written to disk"},"encoding":{"Type":242,"Flags":0,"Description":"Represents secret encodings"},"format":{"Type":245,"Flags":0,"Description":"Represents certificate formats"},"name":{"Type":4,"Flags":1,"Description":"The name of the certificate"},"certType":{"Type":249,"Flags":0,"Description":"Represents certificate types"},"version":{"Type":4,"Flags":0,"Description":"Certificate version"}}}},{"6":{"Value":"utf-8"}},{"6":{"Value":"hex"}},{"6":{"Value":"base64"}},{"5":{"Elements":[239,240,241]}},{"6":{"Value":"pem"}},{"6":{"Value":"pfx"}},{"5":{"Elements":[243,244]}},{"6":{"Value":"certificate"}},{"6":{"Value":"privatekey"}},{"6":{"Value":"publickey"}},{"5":{"Elements":[246,247,248]}},{"2":{"Name":"AzureKeyVaultVolumePropertiesCertificates","Properties":{},"AdditionalProperties":238}},{"2":{"Name":"KeyObjectProperties","Properties":{"alias":{"Type":4,"Flags":0,"Description":"File name when written to disk"},"name":{"Type":4,"Flags":1,"Description":"The name of the key"},"version":{"Type":4,"Flags":0,"Description":"Key version"}}}},{"2":{"Name":"AzureKeyVaultVolumePropertiesKeys","Properties":{},"AdditionalProperties":251}},{"2":{"Name":"SecretObjectProperties","Properties":{"alias":{"Type":4,"Flags":0,"Description":"File name when written to disk"},"encoding":{"Type":257,"Flags":0,"Description":"Represents secret encodings"},"name":{"Type":4,"Flags":1,"Description":"The name of the secret"},"version":{"Type":4,"Flags":0,"Description":"secret version"}}}},{"6":{"Value":"utf-8"}},{"6":{"Value":"hex"}},{"6":{"Value":"base64"}},{"5":{"Elements":[254,255,256]}},{"2":{"Name":"AzureKeyVaultVolumePropertiesSecrets","Properties":{},"AdditionalProperties":253}},{"6":{"Value":"azure.com.keyvault"}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/volumes@2023-10-01-preview","ScopeType":0,"Body":227}},{"8":{"Name":"listSecrets","ResourceType":"Applications.Core/extenders","ApiVersion":"2023-10-01-preview","Output":0,"Input":0}},{"2":{"Name":"SecretStoreListSecretsResult","Properties":{"type":{"Type":266,"Flags":2,"Description":"The type of SecretStore data"},"data":{"Type":267,"Flags":2,"Description":"An object to represent key-value type secrets"}}}},{"6":{"Value":"generic"}},{"6":{"Value":"certificate"}},{"5":{"Elements":[264,265]}},{"2":{"Name":"SecretStoreListSecretsResultData","Properties":{},"AdditionalProperties":217}},{"8":{"Name":"listSecrets","ResourceType":"Applications.Core/secretStores","ApiVersion":"2023-10-01-preview","Output":263,"Input":0}}] \ No newline at end of file +[{"1":{"Kind":1}},{"1":{"Kind":2}},{"1":{"Kind":3}},{"1":{"Kind":4}},{"1":{"Kind":5}},{"1":{"Kind":6}},{"1":{"Kind":7}},{"1":{"Kind":8}},{"6":{"Value":"Applications.Core/applications"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/applications","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":8,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":9,"Flags":10,"Description":"The resource api version"},"properties":{"Type":11,"Flags":1,"Description":"Application properties"},"tags":{"Type":46,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"ApplicationProperties","Properties":{"provisioningState":{"Type":19,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"environment":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"extensions":{"Type":34,"Flags":0,"Description":"The application extension."},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[12,13,14,15,16,17,18]}},{"7":{"Name":"Extension","Discriminator":"kind","BaseProperties":{},"Elements":{"daprSidecar":21,"kubernetesMetadata":26,"kubernetesNamespace":30,"manualScaling":32}}},{"2":{"Name":"DaprSidecarExtension","Properties":{"appPort":{"Type":3,"Flags":0,"Description":"The Dapr appPort. Specifies the internal listening port for the application to handle requests from the Dapr sidecar."},"appId":{"Type":4,"Flags":1,"Description":"The Dapr appId. Specifies the identifier used by Dapr for service invocation."},"config":{"Type":4,"Flags":0,"Description":"Specifies the Dapr configuration to use for the resource."},"protocol":{"Type":24,"Flags":0,"Description":"The Dapr sidecar extension protocol"},"kind":{"Type":25,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"6":{"Value":"http"}},{"6":{"Value":"grpc"}},{"5":{"Elements":[22,23]}},{"6":{"Value":"daprSidecar"}},{"2":{"Name":"KubernetesMetadataExtension","Properties":{"annotations":{"Type":27,"Flags":0,"Description":"Annotations to be applied to the Kubernetes resources output by the resource"},"labels":{"Type":28,"Flags":0,"Description":"Labels to be applied to the Kubernetes resources output by the resource"},"kind":{"Type":29,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"2":{"Name":"KubernetesMetadataExtensionAnnotations","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"KubernetesMetadataExtensionLabels","Properties":{},"AdditionalProperties":4}},{"6":{"Value":"kubernetesMetadata"}},{"2":{"Name":"KubernetesNamespaceExtension","Properties":{"namespace":{"Type":4,"Flags":1,"Description":"The namespace of the application environment."},"kind":{"Type":31,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"6":{"Value":"kubernetesNamespace"}},{"2":{"Name":"ManualScalingExtension","Properties":{"replicas":{"Type":3,"Flags":1,"Description":"Replica count."},"kind":{"Type":33,"Flags":1,"Description":"Discriminator property for Extension."}}}},{"6":{"Value":"manualScaling"}},{"3":{"ItemType":20}},{"2":{"Name":"ResourceStatus","Properties":{"compute":{"Type":36,"Flags":0,"Description":"Represents backing compute resource"},"recipe":{"Type":43,"Flags":2,"Description":"Recipe status at deployment time for a resource."},"outputResources":{"Type":45,"Flags":0,"Description":"Properties of an output resource"}}}},{"7":{"Name":"EnvironmentCompute","Discriminator":"kind","BaseProperties":{"resourceId":{"Type":4,"Flags":0,"Description":"The resource id of the compute resource for application environment."},"identity":{"Type":37,"Flags":0,"Description":"IdentitySettings is the external identity setting."}},"Elements":{"kubernetes":41}}},{"2":{"Name":"IdentitySettings","Properties":{"kind":{"Type":40,"Flags":1,"Description":"IdentitySettingKind is the kind of supported external identity setting"},"oidcIssuer":{"Type":4,"Flags":0,"Description":"The URI for your compute platform's OIDC issuer"},"resource":{"Type":4,"Flags":0,"Description":"The resource ID of the provisioned identity"}}}},{"6":{"Value":"undefined"}},{"6":{"Value":"azure.com.workload"}},{"5":{"Elements":[38,39]}},{"2":{"Name":"KubernetesCompute","Properties":{"namespace":{"Type":4,"Flags":1,"Description":"The namespace to use for the environment."},"kind":{"Type":42,"Flags":1,"Description":"Discriminator property for EnvironmentCompute."}}}},{"6":{"Value":"kubernetes"}},{"2":{"Name":"RecipeStatus","Properties":{"templateKind":{"Type":4,"Flags":1,"Description":"TemplateKind is the kind of the recipe template used by the portable resource upon deployment."},"templatePath":{"Type":4,"Flags":1,"Description":"TemplatePath is the path of the recipe consumed by the portable resource upon deployment."},"templateVersion":{"Type":4,"Flags":0,"Description":"TemplateVersion is the version number of the template."}}}},{"2":{"Name":"OutputResource","Properties":{"localId":{"Type":4,"Flags":0,"Description":"The logical identifier scoped to the owning Radius resource. This is only needed or used when a resource has a dependency relationship. LocalIDs do not have any particular format or meaning beyond being compared to determine dependency relationships."},"id":{"Type":4,"Flags":0,"Description":"The UCP resource ID of the underlying resource."},"radiusManaged":{"Type":2,"Flags":0,"Description":"Determines whether Radius manages the lifecycle of the underlying resource."}}}},{"3":{"ItemType":44}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"SystemData","Properties":{"createdBy":{"Type":4,"Flags":0,"Description":"The identity that created the resource."},"createdByType":{"Type":52,"Flags":0,"Description":"The type of identity that created the resource."},"createdAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource creation (UTC)."},"lastModifiedBy":{"Type":4,"Flags":0,"Description":"The identity that last modified the resource."},"lastModifiedByType":{"Type":57,"Flags":0,"Description":"The type of identity that created the resource."},"lastModifiedAt":{"Type":4,"Flags":0,"Description":"The timestamp of resource last modification (UTC)"}}}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[48,49,50,51]}},{"6":{"Value":"User"}},{"6":{"Value":"Application"}},{"6":{"Value":"ManagedIdentity"}},{"6":{"Value":"Key"}},{"5":{"Elements":[53,54,55,56]}},{"4":{"Name":"Applications.Core/applications@2023-10-01-preview","ScopeType":0,"Body":10}},{"6":{"Value":"Applications.Core/containers"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/containers","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":59,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":60,"Flags":10,"Description":"The resource api version"},"properties":{"Type":62,"Flags":1,"Description":"Container properties"},"tags":{"Type":122,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"ContainerProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":70,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"container":{"Type":71,"Flags":1,"Description":"Definition of a container"},"connections":{"Type":108,"Flags":0,"Description":"Specifies a connection to another resource."},"identity":{"Type":37,"Flags":0,"Description":"IdentitySettings is the external identity setting."},"extensions":{"Type":109,"Flags":0,"Description":"Extensions spec of the resource"},"resourceProvisioning":{"Type":112,"Flags":0,"Description":"Specifies how the underlying service/resource is provisioned and managed. Available values are 'internal', where Radius manages the lifecycle of the resource internally, and 'manual', where a user manages the resource."},"resources":{"Type":114,"Flags":0,"Description":"A collection of references to resources associated with the container"},"restartPolicy":{"Type":118,"Flags":0,"Description":"Restart policy for the container"},"runtimes":{"Type":119,"Flags":0,"Description":"The properties for runtime configuration"}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[63,64,65,66,67,68,69]}},{"2":{"Name":"Container","Properties":{"image":{"Type":4,"Flags":1,"Description":"The registry and image to download and run in your container"},"imagePullPolicy":{"Type":75,"Flags":0,"Description":"The image pull policy for the container"},"env":{"Type":76,"Flags":0,"Description":"environment"},"ports":{"Type":81,"Flags":0,"Description":"container ports"},"readinessProbe":{"Type":82,"Flags":0,"Description":"Properties for readiness/liveness probe"},"livenessProbe":{"Type":82,"Flags":0,"Description":"Properties for readiness/liveness probe"},"volumes":{"Type":101,"Flags":0,"Description":"container volumes"},"command":{"Type":102,"Flags":0,"Description":"Entrypoint array. Overrides the container image's ENTRYPOINT"},"args":{"Type":103,"Flags":0,"Description":"Arguments to the entrypoint. Overrides the container image's CMD"},"workingDir":{"Type":4,"Flags":0,"Description":"Working directory for the container"}}}},{"6":{"Value":"Always"}},{"6":{"Value":"IfNotPresent"}},{"6":{"Value":"Never"}},{"5":{"Elements":[72,73,74]}},{"2":{"Name":"ContainerEnv","Properties":{},"AdditionalProperties":4}},{"2":{"Name":"ContainerPortProperties","Properties":{"containerPort":{"Type":3,"Flags":1,"Description":"The listening port number"},"protocol":{"Type":80,"Flags":0,"Description":"The protocol in use by the port"},"provides":{"Type":4,"Flags":0,"Description":"Specifies a route provided by this port"},"scheme":{"Type":4,"Flags":0,"Description":"Specifies the URL scheme of the communication protocol. Consumers can use the scheme to construct a URL. The value defaults to 'http' or 'https' depending on the port value"},"port":{"Type":3,"Flags":0,"Description":"Specifies the port that will be exposed by this container. Must be set when value different from containerPort is desired"}}}},{"6":{"Value":"TCP"}},{"6":{"Value":"UDP"}},{"5":{"Elements":[78,79]}},{"2":{"Name":"ContainerPorts","Properties":{},"AdditionalProperties":77}},{"7":{"Name":"HealthProbeProperties","Discriminator":"kind","BaseProperties":{"initialDelaySeconds":{"Type":3,"Flags":0,"Description":"Initial delay in seconds before probing for readiness/liveness"},"failureThreshold":{"Type":3,"Flags":0,"Description":"Threshold number of times the probe fails after which a failure would be reported"},"periodSeconds":{"Type":3,"Flags":0,"Description":"Interval for the readiness/liveness probe in seconds"},"timeoutSeconds":{"Type":3,"Flags":0,"Description":"Number of seconds after which the readiness/liveness probe times out. Defaults to 5 seconds"}},"Elements":{"exec":83,"httpGet":85,"tcp":88}}},{"2":{"Name":"ExecHealthProbeProperties","Properties":{"command":{"Type":4,"Flags":1,"Description":"Command to execute to probe readiness/liveness"},"kind":{"Type":84,"Flags":1,"Description":"Discriminator property for HealthProbeProperties."}}}},{"6":{"Value":"exec"}},{"2":{"Name":"HttpGetHealthProbeProperties","Properties":{"containerPort":{"Type":3,"Flags":1,"Description":"The listening port number"},"path":{"Type":4,"Flags":1,"Description":"The route to make the HTTP request on"},"headers":{"Type":86,"Flags":0,"Description":"Custom HTTP headers to add to the get request"},"kind":{"Type":87,"Flags":1,"Description":"Discriminator property for HealthProbeProperties."}}}},{"2":{"Name":"HttpGetHealthProbePropertiesHeaders","Properties":{},"AdditionalProperties":4}},{"6":{"Value":"httpGet"}},{"2":{"Name":"TcpHealthProbeProperties","Properties":{"containerPort":{"Type":3,"Flags":1,"Description":"The listening port number"},"kind":{"Type":89,"Flags":1,"Description":"Discriminator property for HealthProbeProperties."}}}},{"6":{"Value":"tcp"}},{"7":{"Name":"Volume","Discriminator":"kind","BaseProperties":{"mountPath":{"Type":4,"Flags":0,"Description":"The path where the volume is mounted"}},"Elements":{"ephemeral":91,"persistent":96}}},{"2":{"Name":"EphemeralVolume","Properties":{"managedStore":{"Type":94,"Flags":1,"Description":"The managed store for the ephemeral volume"},"kind":{"Type":95,"Flags":1,"Description":"Discriminator property for Volume."}}}},{"6":{"Value":"memory"}},{"6":{"Value":"disk"}},{"5":{"Elements":[92,93]}},{"6":{"Value":"ephemeral"}},{"2":{"Name":"PersistentVolume","Properties":{"permission":{"Type":99,"Flags":0,"Description":"The persistent volume permission"},"source":{"Type":4,"Flags":1,"Description":"The source of the volume"},"kind":{"Type":100,"Flags":1,"Description":"Discriminator property for Volume."}}}},{"6":{"Value":"read"}},{"6":{"Value":"write"}},{"5":{"Elements":[97,98]}},{"6":{"Value":"persistent"}},{"2":{"Name":"ContainerVolumes","Properties":{},"AdditionalProperties":90}},{"3":{"ItemType":4}},{"3":{"ItemType":4}},{"2":{"Name":"ConnectionProperties","Properties":{"source":{"Type":4,"Flags":1,"Description":"The source of the connection"},"disableDefaultEnvVars":{"Type":2,"Flags":0,"Description":"default environment variable override"},"iam":{"Type":105,"Flags":0,"Description":"IAM properties"}}}},{"2":{"Name":"IamProperties","Properties":{"kind":{"Type":106,"Flags":1,"Description":"The kind of IAM provider to configure"},"roles":{"Type":107,"Flags":0,"Description":"RBAC permissions to be assigned on the source resource"}}}},{"6":{"Value":"azure"}},{"3":{"ItemType":4}},{"2":{"Name":"ContainerPropertiesConnections","Properties":{},"AdditionalProperties":104}},{"3":{"ItemType":20}},{"6":{"Value":"internal"}},{"6":{"Value":"manual"}},{"5":{"Elements":[110,111]}},{"2":{"Name":"ResourceReference","Properties":{"id":{"Type":4,"Flags":1,"Description":"Resource id of an existing resource"}}}},{"3":{"ItemType":113}},{"6":{"Value":"Always"}},{"6":{"Value":"OnFailure"}},{"6":{"Value":"Never"}},{"5":{"Elements":[115,116,117]}},{"2":{"Name":"RuntimesProperties","Properties":{"kubernetes":{"Type":120,"Flags":0,"Description":"The runtime configuration properties for Kubernetes"}}}},{"2":{"Name":"KubernetesRuntimeProperties","Properties":{"base":{"Type":4,"Flags":0,"Description":"The serialized YAML manifest which represents the base Kubernetes resources to deploy, such as Deployment, Service, ServiceAccount, Secrets, and ConfigMaps."},"pod":{"Type":121,"Flags":0,"Description":"A strategic merge patch that will be applied to the PodSpec object when this container is being deployed."}}}},{"2":{"Name":"KubernetesPodSpec","Properties":{},"AdditionalProperties":0}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/containers@2023-10-01-preview","ScopeType":0,"Body":61}},{"6":{"Value":"Applications.Core/environments"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/environments","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":124,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":125,"Flags":10,"Description":"The resource api version"},"properties":{"Type":127,"Flags":1,"Description":"Environment properties"},"tags":{"Type":153,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"EnvironmentProperties","Properties":{"provisioningState":{"Type":135,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"compute":{"Type":36,"Flags":1,"Description":"Represents backing compute resource"},"providers":{"Type":136,"Flags":0,"Description":"The Cloud providers configuration"},"simulated":{"Type":2,"Flags":0,"Description":"Simulated environment."},"recipes":{"Type":145,"Flags":0,"Description":"Specifies Recipes linked to the Environment."},"recipeConfig":{"Type":146,"Flags":0,"Description":"Configuration for Recipes. Defines how each type of Recipe should be configured and run."},"extensions":{"Type":152,"Flags":0,"Description":"The environment extension."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[128,129,130,131,132,133,134]}},{"2":{"Name":"Providers","Properties":{"azure":{"Type":137,"Flags":0,"Description":"The Azure cloud provider definition"},"aws":{"Type":138,"Flags":0,"Description":"The AWS cloud provider definition"}}}},{"2":{"Name":"ProvidersAzure","Properties":{"scope":{"Type":4,"Flags":1,"Description":"Target scope for Azure resources to be deployed into. For example: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup'"}}}},{"2":{"Name":"ProvidersAws","Properties":{"scope":{"Type":4,"Flags":1,"Description":"Target scope for AWS resources to be deployed into. For example: '/planes/aws/aws/accounts/000000000000/regions/us-west-2'"}}}},{"7":{"Name":"RecipeProperties","Discriminator":"templateKind","BaseProperties":{"templatePath":{"Type":4,"Flags":1,"Description":"Path to the template provided by the recipe. Currently only link to Azure Container Registry is supported."},"parameters":{"Type":0,"Flags":0,"Description":"Any object"}},"Elements":{"bicep":140,"terraform":142}}},{"2":{"Name":"BicepRecipeProperties","Properties":{"plainHttp":{"Type":2,"Flags":0,"Description":"Connect to the Bicep registry using HTTP (not-HTTPS). This should be used when the registry is known not to support HTTPS, for example in a locally-hosted registry. Defaults to false (use HTTPS/TLS)."},"templateKind":{"Type":141,"Flags":1,"Description":"Discriminator property for RecipeProperties."}}}},{"6":{"Value":"bicep"}},{"2":{"Name":"TerraformRecipeProperties","Properties":{"templateVersion":{"Type":4,"Flags":0,"Description":"Version of the template to deploy. For Terraform recipes using a module registry this is required, but must be omitted for other module sources."},"templateKind":{"Type":143,"Flags":1,"Description":"Discriminator property for RecipeProperties."}}}},{"6":{"Value":"terraform"}},{"2":{"Name":"DictionaryOfRecipeProperties","Properties":{},"AdditionalProperties":139}},{"2":{"Name":"EnvironmentPropertiesRecipes","Properties":{},"AdditionalProperties":144}},{"2":{"Name":"RecipeConfigProperties","Properties":{"terraform":{"Type":147,"Flags":0,"Description":"Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment."}}}},{"2":{"Name":"TerraformConfigProperties","Properties":{"authentication":{"Type":148,"Flags":0,"Description":"Authentication information used to access private Terraform module sources. Supported module sources: Git."}}}},{"2":{"Name":"AuthConfig","Properties":{"git":{"Type":149,"Flags":0,"Description":"Authentication information used to access private Terraform modules from Git repository sources."}}}},{"2":{"Name":"GitAuthConfig","Properties":{"pat":{"Type":151,"Flags":0,"Description":"Personal Access Token (PAT) configuration used to authenticate to Git platforms."}}}},{"2":{"Name":"SecretConfig","Properties":{"secret":{"Type":4,"Flags":0,"Description":"The ID of an Applications.Core/SecretStore resource containing the Git platform personal access token (PAT). The secret store must have a secret named 'pat', containing the PAT value. A secret named 'username' is optional, containing the username associated with the pat. By default no username is specified."}}}},{"2":{"Name":"GitAuthConfigPat","Properties":{},"AdditionalProperties":150}},{"3":{"ItemType":20}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/environments@2023-10-01-preview","ScopeType":0,"Body":126}},{"6":{"Value":"Applications.Core/extenders"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/extenders","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":155,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":156,"Flags":10,"Description":"The resource api version"},"properties":{"Type":158,"Flags":1,"Description":"ExtenderResource portable resource properties"},"tags":{"Type":171,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"ExtenderProperties","Properties":{"environment":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the environment that the portable resource is linked to"},"application":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the application that the portable resource is consumed by (if applicable)"},"provisioningState":{"Type":166,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"secrets":{"Type":0,"Flags":0,"Description":"Any object"},"recipe":{"Type":167,"Flags":0,"Description":"The recipe used to automatically deploy underlying infrastructure for a portable resource"},"resourceProvisioning":{"Type":170,"Flags":0,"Description":"Specifies how the underlying service/resource is provisioned and managed. Available values are 'recipe', where Radius manages the lifecycle of the resource through a Recipe, and 'manual', where a user manages the resource and provides the values."}},"AdditionalProperties":0}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[159,160,161,162,163,164,165]}},{"2":{"Name":"Recipe","Properties":{"name":{"Type":4,"Flags":1,"Description":"The name of the recipe within the environment to use"},"parameters":{"Type":0,"Flags":0,"Description":"Any object"}}}},{"6":{"Value":"recipe"}},{"6":{"Value":"manual"}},{"5":{"Elements":[168,169]}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/extenders@2023-10-01-preview","ScopeType":0,"Body":157}},{"6":{"Value":"Applications.Core/gateways"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/gateways","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":173,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":174,"Flags":10,"Description":"The resource api version"},"properties":{"Type":176,"Flags":1,"Description":"Gateway properties"},"tags":{"Type":192,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"GatewayProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":184,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"internal":{"Type":2,"Flags":0,"Description":"Sets Gateway to not be exposed externally (no public IP address associated). Defaults to false (exposed to internet)."},"hostname":{"Type":185,"Flags":0,"Description":"Declare hostname information for the Gateway. Leaving the hostname empty auto-assigns one: mygateway.myapp.PUBLICHOSTNAMEORIP.nip.io."},"routes":{"Type":187,"Flags":1,"Description":"Routes attached to this Gateway"},"tls":{"Type":188,"Flags":0,"Description":"TLS configuration definition for Gateway resource."},"url":{"Type":4,"Flags":2,"Description":"URL of the gateway resource. Readonly"}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[177,178,179,180,181,182,183]}},{"2":{"Name":"GatewayHostname","Properties":{"prefix":{"Type":4,"Flags":0,"Description":"Specify a prefix for the hostname: myhostname.myapp.PUBLICHOSTNAMEORIP.nip.io. Mutually exclusive with 'fullyQualifiedHostname' and will be overridden if both are defined."},"fullyQualifiedHostname":{"Type":4,"Flags":0,"Description":"Specify a fully-qualified domain name: myapp.mydomain.com. Mutually exclusive with 'prefix' and will take priority if both are defined."}}}},{"2":{"Name":"GatewayRoute","Properties":{"path":{"Type":4,"Flags":0,"Description":"The path to match the incoming request path on. Ex - /myservice."},"destination":{"Type":4,"Flags":0,"Description":"The HttpRoute to route to. Ex - myserviceroute.id."},"replacePrefix":{"Type":4,"Flags":0,"Description":"Optionally update the prefix when sending the request to the service. Ex - replacePrefix: '/' and path: '/myservice' will transform '/myservice/myroute' to '/myroute'"}}}},{"3":{"ItemType":186}},{"2":{"Name":"GatewayTls","Properties":{"sslPassthrough":{"Type":2,"Flags":0,"Description":"If true, gateway lets the https traffic sslPassthrough to the backend servers for decryption."},"minimumProtocolVersion":{"Type":191,"Flags":0,"Description":"Tls Minimum versions for Gateway resource."},"certificateFrom":{"Type":4,"Flags":0,"Description":"The resource id for the secret containing the TLS certificate and key for the gateway."}}}},{"6":{"Value":"1.2"}},{"6":{"Value":"1.3"}},{"5":{"Elements":[189,190]}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/gateways@2023-10-01-preview","ScopeType":0,"Body":175}},{"6":{"Value":"Applications.Core/httpRoutes"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/httpRoutes","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":194,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":195,"Flags":10,"Description":"The resource api version"},"properties":{"Type":197,"Flags":1,"Description":"HTTPRoute properties"},"tags":{"Type":206,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"HttpRouteProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":205,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"hostname":{"Type":4,"Flags":0,"Description":"The internal hostname accepting traffic for the HTTP Route. Readonly."},"port":{"Type":3,"Flags":0,"Description":"The port number for the HTTP Route. Defaults to 80. Readonly."},"scheme":{"Type":4,"Flags":2,"Description":"The scheme used for traffic. Readonly."},"url":{"Type":4,"Flags":2,"Description":"A stable URL that that can be used to route traffic to a resource. Readonly."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[198,199,200,201,202,203,204]}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/httpRoutes@2023-10-01-preview","ScopeType":0,"Body":196}},{"6":{"Value":"Applications.Core/secretStores"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/secretStores","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":208,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":209,"Flags":10,"Description":"The resource api version"},"properties":{"Type":211,"Flags":1,"Description":"The properties of SecretStore"},"tags":{"Type":229,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"2":{"Name":"SecretStoreProperties","Properties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":219,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."},"type":{"Type":222,"Flags":0,"Description":"The type of SecretStore data"},"data":{"Type":228,"Flags":1,"Description":"An object to represent key-value type secrets"},"resource":{"Type":4,"Flags":0,"Description":"The resource id of external secret store."}}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[212,213,214,215,216,217,218]}},{"6":{"Value":"generic"}},{"6":{"Value":"certificate"}},{"5":{"Elements":[220,221]}},{"2":{"Name":"SecretValueProperties","Properties":{"encoding":{"Type":226,"Flags":0,"Description":"The type of SecretValue Encoding"},"value":{"Type":4,"Flags":0,"Description":"The value of secret."},"valueFrom":{"Type":227,"Flags":0,"Description":"The Secret value source properties"}}}},{"6":{"Value":"raw"}},{"6":{"Value":"base64"}},{"5":{"Elements":[224,225]}},{"2":{"Name":"ValueFromProperties","Properties":{"name":{"Type":4,"Flags":1,"Description":"The name of the referenced secret."},"version":{"Type":4,"Flags":0,"Description":"The version of the referenced secret."}}}},{"2":{"Name":"SecretStorePropertiesData","Properties":{},"AdditionalProperties":223}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/secretStores@2023-10-01-preview","ScopeType":0,"Body":210}},{"6":{"Value":"Applications.Core/volumes"}},{"6":{"Value":"2023-10-01-preview"}},{"2":{"Name":"Applications.Core/volumes","Properties":{"id":{"Type":4,"Flags":10,"Description":"The resource id"},"name":{"Type":4,"Flags":9,"Description":"The resource name"},"type":{"Type":231,"Flags":10,"Description":"The resource type"},"apiVersion":{"Type":232,"Flags":10,"Description":"The resource api version"},"properties":{"Type":234,"Flags":1,"Description":"Volume properties"},"tags":{"Type":266,"Flags":0,"Description":"Resource tags."},"location":{"Type":4,"Flags":1,"Description":"The geo-location where the resource lives"},"systemData":{"Type":47,"Flags":2,"Description":"Metadata pertaining to creation and last modification of the resource."}}}},{"7":{"Name":"VolumeProperties","Discriminator":"kind","BaseProperties":{"environment":{"Type":4,"Flags":0,"Description":"Fully qualified resource ID for the environment that the application is linked to"},"application":{"Type":4,"Flags":1,"Description":"Fully qualified resource ID for the application"},"provisioningState":{"Type":242,"Flags":2,"Description":"Provisioning state of the resource at the time the operation was called"},"status":{"Type":35,"Flags":2,"Description":"Status of a resource."}},"Elements":{"azure.com.keyvault":243}}},{"6":{"Value":"Succeeded"}},{"6":{"Value":"Failed"}},{"6":{"Value":"Canceled"}},{"6":{"Value":"Provisioning"}},{"6":{"Value":"Updating"}},{"6":{"Value":"Deleting"}},{"6":{"Value":"Accepted"}},{"5":{"Elements":[235,236,237,238,239,240,241]}},{"2":{"Name":"AzureKeyVaultVolumeProperties","Properties":{"certificates":{"Type":256,"Flags":0,"Description":"The KeyVault certificates that this volume exposes"},"keys":{"Type":258,"Flags":0,"Description":"The KeyVault keys that this volume exposes"},"resource":{"Type":4,"Flags":1,"Description":"The ID of the keyvault to use for this volume resource"},"secrets":{"Type":264,"Flags":0,"Description":"The KeyVault secrets that this volume exposes"},"kind":{"Type":265,"Flags":1,"Description":"Discriminator property for VolumeProperties."}}}},{"2":{"Name":"CertificateObjectProperties","Properties":{"alias":{"Type":4,"Flags":0,"Description":"File name when written to disk"},"encoding":{"Type":248,"Flags":0,"Description":"Represents secret encodings"},"format":{"Type":251,"Flags":0,"Description":"Represents certificate formats"},"name":{"Type":4,"Flags":1,"Description":"The name of the certificate"},"certType":{"Type":255,"Flags":0,"Description":"Represents certificate types"},"version":{"Type":4,"Flags":0,"Description":"Certificate version"}}}},{"6":{"Value":"utf-8"}},{"6":{"Value":"hex"}},{"6":{"Value":"base64"}},{"5":{"Elements":[245,246,247]}},{"6":{"Value":"pem"}},{"6":{"Value":"pfx"}},{"5":{"Elements":[249,250]}},{"6":{"Value":"certificate"}},{"6":{"Value":"privatekey"}},{"6":{"Value":"publickey"}},{"5":{"Elements":[252,253,254]}},{"2":{"Name":"AzureKeyVaultVolumePropertiesCertificates","Properties":{},"AdditionalProperties":244}},{"2":{"Name":"KeyObjectProperties","Properties":{"alias":{"Type":4,"Flags":0,"Description":"File name when written to disk"},"name":{"Type":4,"Flags":1,"Description":"The name of the key"},"version":{"Type":4,"Flags":0,"Description":"Key version"}}}},{"2":{"Name":"AzureKeyVaultVolumePropertiesKeys","Properties":{},"AdditionalProperties":257}},{"2":{"Name":"SecretObjectProperties","Properties":{"alias":{"Type":4,"Flags":0,"Description":"File name when written to disk"},"encoding":{"Type":263,"Flags":0,"Description":"Represents secret encodings"},"name":{"Type":4,"Flags":1,"Description":"The name of the secret"},"version":{"Type":4,"Flags":0,"Description":"secret version"}}}},{"6":{"Value":"utf-8"}},{"6":{"Value":"hex"}},{"6":{"Value":"base64"}},{"5":{"Elements":[260,261,262]}},{"2":{"Name":"AzureKeyVaultVolumePropertiesSecrets","Properties":{},"AdditionalProperties":259}},{"6":{"Value":"azure.com.keyvault"}},{"2":{"Name":"TrackedResourceTags","Properties":{},"AdditionalProperties":4}},{"4":{"Name":"Applications.Core/volumes@2023-10-01-preview","ScopeType":0,"Body":233}},{"8":{"Name":"listSecrets","ResourceType":"Applications.Core/extenders","ApiVersion":"2023-10-01-preview","Output":0,"Input":0}},{"2":{"Name":"SecretStoreListSecretsResult","Properties":{"type":{"Type":272,"Flags":2,"Description":"The type of SecretStore data"},"data":{"Type":273,"Flags":2,"Description":"An object to represent key-value type secrets"}}}},{"6":{"Value":"generic"}},{"6":{"Value":"certificate"}},{"5":{"Elements":[270,271]}},{"2":{"Name":"SecretStoreListSecretsResultData","Properties":{},"AdditionalProperties":223}},{"8":{"Name":"listSecrets","ResourceType":"Applications.Core/secretStores","ApiVersion":"2023-10-01-preview","Output":269,"Input":0}}] \ No newline at end of file diff --git a/hack/bicep-types-radius/generated/index.json b/hack/bicep-types-radius/generated/index.json index 735c06d6d0..927e764089 100644 --- a/hack/bicep-types-radius/generated/index.json +++ b/hack/bicep-types-radius/generated/index.json @@ -1 +1 @@ -{"Resources":{"Applications.Core/applications@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":58},"Applications.Core/containers@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":123},"Applications.Core/environments@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":148},"Applications.Core/extenders@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":166},"Applications.Core/gateways@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":187},"Applications.Core/httpRoutes@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":201},"Applications.Core/secretStores@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":224},"Applications.Core/volumes@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":261},"Applications.Dapr/pubSubBrokers@2023-10-01-preview":{"RelativePath":"applications/applications.dapr/2023-10-01-preview/types.json","Index":49},"Applications.Dapr/secretStores@2023-10-01-preview":{"RelativePath":"applications/applications.dapr/2023-10-01-preview/types.json","Index":66},"Applications.Dapr/stateStores@2023-10-01-preview":{"RelativePath":"applications/applications.dapr/2023-10-01-preview/types.json","Index":84},"Applications.Datastores/mongoDatabases@2023-10-01-preview":{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":50},"Applications.Datastores/redisCaches@2023-10-01-preview":{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":69},"Applications.Datastores/sqlDatabases@2023-10-01-preview":{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":88},"Applications.Messaging/rabbitMQQueues@2023-10-01-preview":{"RelativePath":"applications/applications.messaging/2023-10-01-preview/types.json","Index":50}},"Functions":{"applications.core/extenders":{"2023-10-01-preview":[{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":262}]},"applications.core/secretstores":{"2023-10-01-preview":[{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":268}]},"applications.datastores/mongodatabases":{"2023-10-01-preview":[{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":90}]},"applications.datastores/rediscaches":{"2023-10-01-preview":[{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":92}]},"applications.datastores/sqldatabases":{"2023-10-01-preview":[{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":94}]},"applications.messaging/rabbitmqqueues":{"2023-10-01-preview":[{"RelativePath":"applications/applications.messaging/2023-10-01-preview/types.json","Index":52}]}}} \ No newline at end of file +{"Resources":{"Applications.Core/applications@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":58},"Applications.Core/containers@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":123},"Applications.Core/environments@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":154},"Applications.Core/extenders@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":172},"Applications.Core/gateways@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":193},"Applications.Core/httpRoutes@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":207},"Applications.Core/secretStores@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":230},"Applications.Core/volumes@2023-10-01-preview":{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":267},"Applications.Dapr/pubSubBrokers@2023-10-01-preview":{"RelativePath":"applications/applications.dapr/2023-10-01-preview/types.json","Index":49},"Applications.Dapr/secretStores@2023-10-01-preview":{"RelativePath":"applications/applications.dapr/2023-10-01-preview/types.json","Index":66},"Applications.Dapr/stateStores@2023-10-01-preview":{"RelativePath":"applications/applications.dapr/2023-10-01-preview/types.json","Index":84},"Applications.Datastores/mongoDatabases@2023-10-01-preview":{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":50},"Applications.Datastores/redisCaches@2023-10-01-preview":{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":69},"Applications.Datastores/sqlDatabases@2023-10-01-preview":{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":88},"Applications.Messaging/rabbitMQQueues@2023-10-01-preview":{"RelativePath":"applications/applications.messaging/2023-10-01-preview/types.json","Index":50}},"Functions":{"applications.core/extenders":{"2023-10-01-preview":[{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":268}]},"applications.core/secretstores":{"2023-10-01-preview":[{"RelativePath":"applications/applications.core/2023-10-01-preview/types.json","Index":274}]},"applications.datastores/mongodatabases":{"2023-10-01-preview":[{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":90}]},"applications.datastores/rediscaches":{"2023-10-01-preview":[{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":92}]},"applications.datastores/sqldatabases":{"2023-10-01-preview":[{"RelativePath":"applications/applications.datastores/2023-10-01-preview/types.json","Index":94}]},"applications.messaging/rabbitmqqueues":{"2023-10-01-preview":[{"RelativePath":"applications/applications.messaging/2023-10-01-preview/types.json","Index":52}]}}} \ No newline at end of file diff --git a/pkg/corerp/api/v20231001preview/environment_conversion.go b/pkg/corerp/api/v20231001preview/environment_conversion.go index 1b083b1adb..a5d2ff2d6f 100644 --- a/pkg/corerp/api/v20231001preview/environment_conversion.go +++ b/pkg/corerp/api/v20231001preview/environment_conversion.go @@ -18,6 +18,7 @@ package v20231001preview import ( "fmt" + "reflect" "strings" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" @@ -62,6 +63,7 @@ func (src *EnvironmentResource) ConvertTo() (v1.DataModelInterface, error) { return nil, err } converted.Properties.Compute = *envCompute + converted.Properties.RecipeConfig = toRecipeConfigDatamodel(src.Properties.RecipeConfig) if src.Properties.Recipes != nil { envRecipes := make(map[string]map[string]datamodel.EnvironmentRecipeProperties) @@ -150,6 +152,7 @@ func (dst *EnvironmentResource) ConvertFrom(src v1.DataModelInterface) error { } dst.Properties.Recipes = recipes } + dst.Properties.RecipeConfig = fromRecipeConfigDatamodel(env.Properties.RecipeConfig) if env.Properties.Providers != (datamodel.Providers{}) { dst.Properties.Providers = &Providers{} @@ -180,6 +183,58 @@ func (dst *EnvironmentResource) ConvertFrom(src v1.DataModelInterface) error { return nil } +func toRecipeConfigDatamodel(config *RecipeConfigProperties) datamodel.RecipeConfigProperties { + if config != nil { + recipeConfig := datamodel.RecipeConfigProperties{} + if config.Terraform != nil { + recipeConfig.Terraform = datamodel.TerraformConfigProperties{} + if config.Terraform.Authentication != nil { + recipeConfig.Terraform.Authentication = datamodel.AuthConfig{} + gitConfig := config.Terraform.Authentication.Git + if gitConfig != nil { + recipeConfig.Terraform.Authentication.Git = datamodel.GitAuthConfig{} + if gitConfig.Pat != nil { + p := map[string]datamodel.SecretConfig{} + for k, v := range gitConfig.Pat { + p[k] = datamodel.SecretConfig{ + Secret: to.String(v.Secret), + } + } + recipeConfig.Terraform.Authentication.Git.PAT = p + } + } + } + } + return recipeConfig + } + return datamodel.RecipeConfigProperties{} +} + +func fromRecipeConfigDatamodel(config datamodel.RecipeConfigProperties) *RecipeConfigProperties { + if !reflect.DeepEqual(config, datamodel.RecipeConfigProperties{}) { + recipeConfig := &RecipeConfigProperties{} + if !reflect.DeepEqual(config.Terraform, datamodel.TerraformConfigProperties{}) { + recipeConfig.Terraform = &TerraformConfigProperties{} + if !reflect.DeepEqual(config.Terraform.Authentication, datamodel.AuthConfig{}) { + recipeConfig.Terraform.Authentication = &AuthConfig{} + if !reflect.DeepEqual(config.Terraform.Authentication.Git, datamodel.GitAuthConfig{}) { + recipeConfig.Terraform.Authentication.Git = &GitAuthConfig{} + if config.Terraform.Authentication.Git.PAT != nil { + recipeConfig.Terraform.Authentication.Git.Pat = map[string]*SecretConfig{} + for k, v := range config.Terraform.Authentication.Git.PAT { + recipeConfig.Terraform.Authentication.Git.Pat[k] = &SecretConfig{ + Secret: to.Ptr(v.Secret), + } + } + } + } + } + } + return recipeConfig + } + return nil +} + func toEnvironmentComputeDataModel(h EnvironmentComputeClassification) (*rpv1.EnvironmentCompute, error) { switch v := h.(type) { case *KubernetesCompute: diff --git a/pkg/corerp/api/v20231001preview/environment_conversion_test.go b/pkg/corerp/api/v20231001preview/environment_conversion_test.go index 6f8ba179ae..68d61c7050 100644 --- a/pkg/corerp/api/v20231001preview/environment_conversion_test.go +++ b/pkg/corerp/api/v20231001preview/environment_conversion_test.go @@ -73,6 +73,13 @@ func TestConvertVersionedToDataModel(t *testing.T) { Scope: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup", }, }, + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Authentication: datamodel.AuthConfig{ + Git: datamodel.GitAuthConfig{}, + }, + }, + }, Recipes: map[string]map[string]datamodel.EnvironmentRecipeProperties{ ds_ctrl.MongoDatabasesResourceType: { "cosmos-recipe": datamodel.EnvironmentRecipeProperties{ @@ -117,6 +124,19 @@ func TestConvertVersionedToDataModel(t *testing.T) { Scope: "/planes/aws/aws/accounts/140313373712/regions/us-west-2", }, }, + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Authentication: datamodel.AuthConfig{ + Git: datamodel.GitAuthConfig{ + PAT: map[string]datamodel.SecretConfig{ + "dev.azure.com": { + Secret: "/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github", + }, + }, + }, + }, + }, + }, Recipes: map[string]map[string]datamodel.EnvironmentRecipeProperties{ ds_ctrl.MongoDatabasesResourceType: { "cosmos-recipe": datamodel.EnvironmentRecipeProperties{ @@ -368,6 +388,7 @@ func TestConvertDataModelToVersioned(t *testing.T) { if tt.filename == "environmentresourcedatamodel.json" { require.Equal(t, "Azure/cosmosdb/azurerm", string(*versioned.Properties.Recipes[ds_ctrl.MongoDatabasesResourceType]["terraform-recipe"].GetRecipeProperties().TemplatePath)) require.Equal(t, recipes.TemplateKindTerraform, string(*versioned.Properties.Recipes[ds_ctrl.MongoDatabasesResourceType]["terraform-recipe"].GetRecipeProperties().TemplateKind)) + require.Equal(t, "/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github", string(*versioned.Properties.RecipeConfig.Terraform.Authentication.Git.Pat["dev.azure.com"].Secret)) switch c := recipeDetails.(type) { case *TerraformRecipeProperties: require.Equal(t, "1.1.0", string(*c.TemplateVersion)) diff --git a/pkg/corerp/api/v20231001preview/testdata/environmentresource-with-workload-identity.json b/pkg/corerp/api/v20231001preview/testdata/environmentresource-with-workload-identity.json index 071dcd34d0..11ce3dc424 100644 --- a/pkg/corerp/api/v20231001preview/testdata/environmentresource-with-workload-identity.json +++ b/pkg/corerp/api/v20231001preview/testdata/environmentresource-with-workload-identity.json @@ -18,6 +18,13 @@ "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": {} + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/pkg/corerp/api/v20231001preview/testdata/environmentresource.json b/pkg/corerp/api/v20231001preview/testdata/environmentresource.json index 371de02ab6..e89fe6d6c7 100644 --- a/pkg/corerp/api/v20231001preview/testdata/environmentresource.json +++ b/pkg/corerp/api/v20231001preview/testdata/environmentresource.json @@ -16,6 +16,19 @@ "scope": "/planes/aws/aws/accounts/140313373712/regions/us-west-2" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel-with-workload-identity.json b/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel-with-workload-identity.json index 19cee1e6c0..c66221ed4b 100644 --- a/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel-with-workload-identity.json +++ b/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel-with-workload-identity.json @@ -31,6 +31,13 @@ "scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": {} + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel.json b/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel.json index fd2d29a3d4..fb126cef61 100644 --- a/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel.json +++ b/pkg/corerp/api/v20231001preview/testdata/environmentresourcedatamodel.json @@ -29,6 +29,19 @@ "scope": "/planes/aws/aws/accounts/140313373712/regions/us-west-2" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/pkg/corerp/api/v20231001preview/zz_generated_models.go b/pkg/corerp/api/v20231001preview/zz_generated_models.go index 72a5e0b8c5..1a351d8ce5 100644 --- a/pkg/corerp/api/v20231001preview/zz_generated_models.go +++ b/pkg/corerp/api/v20231001preview/zz_generated_models.go @@ -124,6 +124,12 @@ type ApplicationResourceUpdateProperties struct { Extensions []ExtensionClassification } +// AuthConfig - Authentication information used to access private Terraform module sources. Supported module sources: Git. +type AuthConfig struct { + // Authentication information used to access private Terraform modules from Git repository sources. + Git *GitAuthConfig +} + // AzureKeyVaultVolumeProperties - Represents Azure Key Vault Volume properties type AzureKeyVaultVolumeProperties struct { // REQUIRED; Fully qualified resource ID for the application @@ -544,6 +550,9 @@ type EnvironmentProperties struct { // Cloud providers configuration for the environment. Providers *Providers + // Configuration for Recipes. Defines how each type of Recipe should be configured and run. + RecipeConfig *RecipeConfigProperties + // Specifies Recipes linked to the Environment. Recipes map[string]map[string]RecipePropertiesClassification @@ -607,6 +616,9 @@ type EnvironmentResourceUpdateProperties struct { // Cloud providers configuration for the environment. Providers *ProvidersUpdate + // Configuration for Recipes. Defines how each type of Recipe should be configured and run. + RecipeConfig *RecipeConfigProperties + // Specifies Recipes linked to the Environment. Recipes map[string]map[string]RecipePropertiesUpdateClassification @@ -925,6 +937,12 @@ type GatewayTLS struct { SSLPassthrough *bool } +// GitAuthConfig - Authentication information used to access private Terraform modules from Git repository sources. +type GitAuthConfig struct { + // Personal Access Token (PAT) configuration used to authenticate to Git platforms. + Pat map[string]*SecretConfig +} + // HTTPGetHealthProbeProperties - Specifies the properties for readiness/liveness probe using HTTP Get type HTTPGetHealthProbeProperties struct { // REQUIRED; The listening port number @@ -1368,6 +1386,12 @@ type Recipe struct { Parameters map[string]any } +// RecipeConfigProperties - Configuration for Recipes. Defines how each type of Recipe should be configured and run. +type RecipeConfigProperties struct { + // Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment. + Terraform *TerraformConfigProperties +} + // RecipeGetMetadata - Represents the request body of the getmetadata action. type RecipeGetMetadata struct { // REQUIRED; The name of the recipe registered to the environment @@ -1487,6 +1511,14 @@ type RuntimesProperties struct { Kubernetes *KubernetesRuntimeProperties } +// SecretConfig - Personal Access Token (PAT) configuration used to authenticate to Git platforms. +type SecretConfig struct { + // The ID of an Applications.Core/SecretStore resource containing the Git platform personal access token (PAT). The secret +// store must have a secret named 'pat', containing the PAT value. A secret named +// 'username' is optional, containing the username associated with the pat. By default no username is specified. + Secret *string +} + // SecretObjectProperties - Represents secret object properties type SecretObjectProperties struct { // REQUIRED; The name of the secret @@ -1660,6 +1692,13 @@ func (t *TCPHealthProbeProperties) GetHealthProbeProperties() *HealthProbeProper } } +// TerraformConfigProperties - Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as +// part of Recipe deployment. +type TerraformConfigProperties struct { + // Authentication information used to access private Terraform module sources. Supported module sources: Git. + Authentication *AuthConfig +} + // TerraformRecipeProperties - Represents Terraform recipe properties. type TerraformRecipeProperties struct { // REQUIRED; Discriminator property for RecipeProperties. diff --git a/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go b/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go index c1b39b8004..3af7888308 100644 --- a/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go +++ b/pkg/corerp/api/v20231001preview/zz_generated_models_serde.go @@ -337,6 +337,33 @@ func (a *ApplicationResourceUpdateProperties) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type AuthConfig. +func (a AuthConfig) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "git", a.Git) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type AuthConfig. +func (a *AuthConfig) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "git": + err = unpopulate(val, "Git", &a.Git) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", a, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type AzureKeyVaultVolumeProperties. func (a AzureKeyVaultVolumeProperties) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) @@ -1170,6 +1197,7 @@ func (e EnvironmentProperties) MarshalJSON() ([]byte, error) { populate(objectMap, "extensions", e.Extensions) populate(objectMap, "providers", e.Providers) populate(objectMap, "provisioningState", e.ProvisioningState) + populate(objectMap, "recipeConfig", e.RecipeConfig) populate(objectMap, "recipes", e.Recipes) populate(objectMap, "simulated", e.Simulated) return json.Marshal(objectMap) @@ -1196,6 +1224,9 @@ func (e *EnvironmentProperties) UnmarshalJSON(data []byte) error { case "provisioningState": err = unpopulate(val, "ProvisioningState", &e.ProvisioningState) delete(rawMsg, key) + case "recipeConfig": + err = unpopulate(val, "RecipeConfig", &e.RecipeConfig) + delete(rawMsg, key) case "recipes": var recipesRaw map[string]json.RawMessage if err = json.Unmarshal(val, &recipesRaw); err != nil { @@ -1340,6 +1371,7 @@ func (e EnvironmentResourceUpdateProperties) MarshalJSON() ([]byte, error) { populate(objectMap, "compute", e.Compute) populate(objectMap, "extensions", e.Extensions) populate(objectMap, "providers", e.Providers) + populate(objectMap, "recipeConfig", e.RecipeConfig) populate(objectMap, "recipes", e.Recipes) populate(objectMap, "simulated", e.Simulated) return json.Marshal(objectMap) @@ -1363,6 +1395,9 @@ func (e *EnvironmentResourceUpdateProperties) UnmarshalJSON(data []byte) error { case "providers": err = unpopulate(val, "Providers", &e.Providers) delete(rawMsg, key) + case "recipeConfig": + err = unpopulate(val, "RecipeConfig", &e.RecipeConfig) + delete(rawMsg, key) case "recipes": var recipesRaw map[string]json.RawMessage if err = json.Unmarshal(val, &recipesRaw); err != nil { @@ -2140,6 +2175,33 @@ func (g *GatewayTLS) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type GitAuthConfig. +func (g GitAuthConfig) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "pat", g.Pat) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type GitAuthConfig. +func (g *GitAuthConfig) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", g, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "pat": + err = unpopulate(val, "Pat", &g.Pat) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", g, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type HTTPGetHealthProbeProperties. func (h HTTPGetHealthProbeProperties) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) @@ -3206,6 +3268,33 @@ func (r *Recipe) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type RecipeConfigProperties. +func (r RecipeConfigProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "terraform", r.Terraform) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type RecipeConfigProperties. +func (r *RecipeConfigProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", r, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "terraform": + err = unpopulate(val, "Terraform", &r.Terraform) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", r, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type RecipeGetMetadata. func (r RecipeGetMetadata) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) @@ -3544,6 +3633,33 @@ func (r *RuntimesProperties) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type SecretConfig. +func (s SecretConfig) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "secret", s.Secret) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type SecretConfig. +func (s *SecretConfig) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", s, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "secret": + err = unpopulate(val, "Secret", &s.Secret) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", s, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type SecretObjectProperties. func (s SecretObjectProperties) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) @@ -3950,6 +4066,33 @@ func (t *TCPHealthProbeProperties) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type TerraformConfigProperties. +func (t TerraformConfigProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "authentication", t.Authentication) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type TerraformConfigProperties. +func (t *TerraformConfigProperties) UnmarshalJSON(data []byte) error { + var rawMsg map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMsg); err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + for key, val := range rawMsg { + var err error + switch key { + case "authentication": + err = unpopulate(val, "Authentication", &t.Authentication) + delete(rawMsg, key) + } + if err != nil { + return fmt.Errorf("unmarshalling type %T: %v", t, err) + } + } + return nil +} + // MarshalJSON implements the json.Marshaller interface for type TerraformRecipeProperties. func (t TerraformRecipeProperties) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) diff --git a/pkg/corerp/datamodel/environment.go b/pkg/corerp/datamodel/environment.go index d74c638144..7e3c7d8ba1 100644 --- a/pkg/corerp/datamodel/environment.go +++ b/pkg/corerp/datamodel/environment.go @@ -38,11 +38,12 @@ func (e *Environment) ResourceTypeName() string { // EnvironmentProperties represents the properties of Environment. type EnvironmentProperties struct { - Compute rpv1.EnvironmentCompute `json:"compute,omitempty"` - Recipes map[string]map[string]EnvironmentRecipeProperties `json:"recipes,omitempty"` - Providers Providers `json:"providers,omitempty"` - Extensions []Extension `json:"extensions,omitempty"` - Simulated bool `json:"simulated,omitempty"` + Compute rpv1.EnvironmentCompute `json:"compute,omitempty"` + Recipes map[string]map[string]EnvironmentRecipeProperties `json:"recipes,omitempty"` + Providers Providers `json:"providers,omitempty"` + RecipeConfig RecipeConfigProperties `json:"recipeConfig,omitempty"` + Extensions []Extension `json:"extensions,omitempty"` + Simulated bool `json:"simulated,omitempty"` } // EnvironmentRecipeProperties represents the properties of environment's recipe. diff --git a/pkg/corerp/datamodel/recipe_types.go b/pkg/corerp/datamodel/recipe_types.go new file mode 100644 index 0000000000..79f8923b5f --- /dev/null +++ b/pkg/corerp/datamodel/recipe_types.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 datamodel + +// RecipeConfigProperties - Configuration for Recipes. Defines how each type of Recipe should be configured and run. +type RecipeConfigProperties struct { + // Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment. + Terraform TerraformConfigProperties `json:"terraform,omitempty"` +} + +// TerraformConfigProperties - Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as +// part of Recipe deployment. +type TerraformConfigProperties struct { + // Authentication information used to access private Terraform module sources. Supported module sources: Git. + Authentication AuthConfig `json:"authentication,omitempty"` +} + +// AuthConfig - Authentication information used to access private Terraform module sources. Supported module sources: Git. +type AuthConfig struct { + // Authentication information used to access private Terraform modules from Git repository sources. + Git GitAuthConfig `json:"git,omitempty"` +} + +// GitAuthConfig - Authentication information used to access private Terraform modules from Git repository sources. +type GitAuthConfig struct { + // Personal Access Token (PAT) configuration used to authenticate to Git platforms. + PAT map[string]SecretConfig `json:"pat,omitempty"` +} + +// SecretConfig - Personal Access Token (PAT) configuration used to authenticate to Git platforms. +type SecretConfig struct { + // The ID of an Applications.Core/SecretStore resource containing the Git platform personal access token (PAT). The secret + // store must have a secret named 'pat', containing the PAT value. A secret named + // 'username' is optional, containing the username associated with the pat. By default no username is specified. + Secret string `json:"secret,omitempty"` +} diff --git a/pkg/recipes/configloader/environment.go b/pkg/recipes/configloader/environment.go index 108fb3bb49..66ae542f21 100644 --- a/pkg/recipes/configloader/environment.go +++ b/pkg/recipes/configloader/environment.go @@ -28,7 +28,6 @@ import ( recipes_util "github.com/radius-project/radius/pkg/recipes/util" "github.com/radius-project/radius/pkg/rp/kube" "github.com/radius-project/radius/pkg/rp/util" - "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/pkg/ucp/resources" ) @@ -72,8 +71,9 @@ func (e *environmentLoader) LoadConfiguration(ctx context.Context, recipe recipe func getConfiguration(environment *v20231001preview.EnvironmentResource, application *v20231001preview.ApplicationResource) (*recipes.Configuration, error) { config := recipes.Configuration{ - Runtime: recipes.RuntimeConfiguration{}, - Providers: datamodel.Providers{}, + Runtime: recipes.RuntimeConfiguration{}, + Providers: datamodel.Providers{}, + RecipeConfig: datamodel.RecipeConfigProperties{}, } switch environment.Properties.Compute.(type) { @@ -101,14 +101,19 @@ func getConfiguration(environment *v20231001preview.EnvironmentResource, applica return nil, ErrUnsupportedComputeKind } - providers := environment.Properties.Providers - if providers != nil { - if providers.Aws != nil { - config.Providers.AWS.Scope = to.String(providers.Aws.Scope) - } - if providers.Azure != nil { - config.Providers.Azure.Scope = to.String(providers.Azure.Scope) - } + // convert versioned Environment resource to internal datamodel. + env, err := environment.ConvertTo() + if err != nil { + return nil, err + } + + envDatamodel := env.(*datamodel.Environment) + if environment.Properties.Providers != nil { + config.Providers = envDatamodel.Properties.Providers + } + + if environment.Properties.RecipeConfig != nil { + config.RecipeConfig = envDatamodel.Properties.RecipeConfig } if environment.Properties.Simulated != nil && *environment.Properties.Simulated { diff --git a/pkg/recipes/configloader/environment_test.go b/pkg/recipes/configloader/environment_test.go index 24b8f01dbf..d6de288e44 100644 --- a/pkg/recipes/configloader/environment_test.go +++ b/pkg/recipes/configloader/environment_test.go @@ -63,6 +63,19 @@ func TestGetConfiguration(t *testing.T) { Scope: to.Ptr(azureScope), }, }, + RecipeConfig: &model.RecipeConfigProperties{ + Terraform: &model.TerraformConfigProperties{ + Authentication: &model.AuthConfig{ + Git: &model.GitAuthConfig{ + Pat: map[string]*model.SecretConfig{ + "dev.azure.com": { + Secret: to.Ptr("/planes/radius/local/resourceGroups/testGroup/providers/Applications.Core/secretStores/secret"), + }, + }, + }, + }, + }, + }, }, }, appResource: nil, @@ -74,6 +87,19 @@ func TestGetConfiguration(t *testing.T) { }, }, Providers: createAzureProvider(), + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Authentication: datamodel.AuthConfig{ + Git: datamodel.GitAuthConfig{ + PAT: map[string]datamodel.SecretConfig{ + "dev.azure.com": { + Secret: "/planes/radius/local/resourceGroups/testGroup/providers/Applications.Core/secretStores/secret", + }, + }, + }, + }, + }, + }, }, }, { @@ -85,6 +111,15 @@ func TestGetConfiguration(t *testing.T) { Namespace: to.Ptr(envNamespace), ResourceID: to.Ptr(envResourceId), }, + RecipeConfig: &model.RecipeConfigProperties{ + Terraform: &model.TerraformConfigProperties{ + Authentication: &model.AuthConfig{ + Git: &model.GitAuthConfig{ + Pat: map[string]*model.SecretConfig{}, + }, + }, + }, + }, Providers: &model.Providers{ Aws: &model.ProvidersAws{ Scope: to.Ptr(awsScope), @@ -100,6 +135,15 @@ func TestGetConfiguration(t *testing.T) { EnvironmentNamespace: envNamespace, }, }, + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Authentication: datamodel.AuthConfig{ + Git: datamodel.GitAuthConfig{ + PAT: map[string]datamodel.SecretConfig{}, + }, + }, + }, + }, Providers: createAWSProvider(), }, }, diff --git a/pkg/recipes/configloader/secrets.go b/pkg/recipes/configloader/secrets.go new file mode 100644 index 0000000000..9c2707e070 --- /dev/null +++ b/pkg/recipes/configloader/secrets.go @@ -0,0 +1,52 @@ +/* +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 configloader + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" + "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + "github.com/radius-project/radius/pkg/ucp/resources" +) + +// NewSecretStoreLoader creates a new SecretsLoader instance with the given ARM Client Options. +func NewSecretStoreLoader(armOptions *arm.ClientOptions) SecretsLoader { + return SecretsLoader{ArmClientOptions: armOptions} +} + +// SecretsLoader struct provides functionality to get secret information from Application.Core/SecretStore resource. +type SecretsLoader struct { + ArmClientOptions *arm.ClientOptions +} + +func (e *SecretsLoader) LoadSecrets(ctx context.Context, secretStore string) (v20231001preview.SecretStoresClientListSecretsResponse, error) { + secretStoreID, err := resources.ParseResource(secretStore) + if err != nil { + return v20231001preview.SecretStoresClientListSecretsResponse{}, err + } + + client, err := v20231001preview.NewSecretStoresClient(secretStoreID.RootScope(), &aztoken.AnonymousCredential{}, e.ArmClientOptions) + if err != nil { + return v20231001preview.SecretStoresClientListSecretsResponse{}, err + } + + secrets, err := client.ListSecrets(ctx, secretStoreID.Name(), map[string]any{}, nil) + if err != nil { + return v20231001preview.SecretStoresClientListSecretsResponse{}, err + } + + return secrets, nil +} diff --git a/pkg/recipes/controllerconfig/config.go b/pkg/recipes/controllerconfig/config.go index 750ffbbf28..6ddeb1ef19 100644 --- a/pkg/recipes/controllerconfig/config.go +++ b/pkg/recipes/controllerconfig/config.go @@ -89,6 +89,7 @@ func New(options hostoptions.HostOptions) (*RecipeControllerConfig, error) { cfg.ConfigLoader = configloader.NewEnvironmentLoader(clientOptions) cfg.Engine = engine.NewEngine(engine.Options{ ConfigurationLoader: cfg.ConfigLoader, + SecretsLoader: configloader.NewSecretStoreLoader(clientOptions), Drivers: map[string]driver.Driver{ recipes.TemplateKindBicep: driver.NewBicepDriver( clientOptions, diff --git a/pkg/recipes/driver/gitconfig.go b/pkg/recipes/driver/gitconfig.go new file mode 100644 index 0000000000..f8a00b949e --- /dev/null +++ b/pkg/recipes/driver/gitconfig.go @@ -0,0 +1,106 @@ +/* +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 driver + +import ( + "errors" + "fmt" + "net/url" + "os/exec" + + "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + "github.com/radius-project/radius/pkg/recipes" +) + +// getGitURLWithSecrets returns the git URL with secrets information added. +func getGitURLWithSecrets(secrets v20231001preview.SecretStoresClientListSecretsResponse, url *url.URL) string { + // accessing the secret values and creating the git url with secret information. + var username, pat *string + path := "https://" + user, ok := secrets.Data["username"] + if ok { + username = user.Value + path += fmt.Sprintf("%s:", *username) + } + + token, ok := secrets.Data["pat"] + if ok { + pat = token.Value + path += *pat + } + path += fmt.Sprintf("@%s", url.Hostname()) + + return path +} + +// getURLConfigKeyValue is used to get the key and value details of the url config. +// get the secret values pat and username from secrets and create a git url in +// the format : https://:@.com and adds it to gitconfig +func getURLConfigKeyValue(secrets v20231001preview.SecretStoresClientListSecretsResponse, templatePath string) (string, string, error) { + url, err := recipes.GetGitURL(templatePath) + if err != nil { + return "", "", err + } + + path := getGitURLWithSecrets(secrets, url) + + // git config key will be in the format of url..insteadOf + // and value returned will the original git url domain, e.g github.com + return fmt.Sprintf("url.%s.insteadOf", path), url.Hostname(), nil +} + +// Updates the global Git configuration with credentials for a recipe template path and prefixes the path with environment, application, and resource name to make the entry unique to each recipe execution operation. +// +// Retrieves the git credentials from the provided secrets object +// and adds them to the Git config by running +// git config --global url.insteadOf . +func addSecretsToGitConfig(secrets v20231001preview.SecretStoresClientListSecretsResponse, recipeMetadata *recipes.ResourceMetadata, templatePath string) error { + urlConfigKey, urlConfigValue, err := getURLConfigKeyValue(secrets, templatePath) + if err != nil { + return err + } + + prefix, err := recipes.GetURLPrefix(recipeMetadata) + if err != nil { + return err + } + urlConfigValue = fmt.Sprintf("%s%s", prefix, urlConfigValue) + cmd := exec.Command("git", "config", "--global", urlConfigKey, urlConfigValue) + _, err = cmd.Output() + if err != nil { + return errors.New("failed to add git config") + } + + return nil +} + +// Unset the git credentials information from .gitconfig by running +// git config --global --unset url.insteadOf +func unsetSecretsFromGitConfig(secrets v20231001preview.SecretStoresClientListSecretsResponse, templatePath string) error { + urlConfigKey, _, err := getURLConfigKeyValue(secrets, templatePath) + if err != nil { + return err + } + + cmd := exec.Command("git", "config", "--global", "--unset", urlConfigKey) + _, err = cmd.Output() + if err != nil { + return errors.New("failed to unset git config") + } + + return nil +} diff --git a/pkg/recipes/driver/gitconfig_test.go b/pkg/recipes/driver/gitconfig_test.go new file mode 100644 index 0000000000..f7a11329a3 --- /dev/null +++ b/pkg/recipes/driver/gitconfig_test.go @@ -0,0 +1,164 @@ +/* +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 driver + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + "github.com/radius-project/radius/pkg/to" + "github.com/stretchr/testify/require" +) + +func TestAddConfig(t *testing.T) { + tests := []struct { + desc string + templatePath string + expectedResponse string + expectedErr error + }{ + { + desc: "success", + templatePath: "git::https://github.com/project/module", + expectedResponse: "[url \"https://test-user:ghp_token@github.com\"]\n\tinsteadOf = https://env1-app1-test-redis-recipe-github.com\n", + expectedErr: nil, + }, + { + desc: "invalid git url", + templatePath: "git::https://gith ub.com/project/module", + expectedErr: errors.New("failed to parse git url"), + }, + { + desc: "invalid resource id", + templatePath: "git::https://github.com/project/module", + expectedErr: errors.New(" is not a valid resource id"), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + tmpdir := t.TempDir() + config, err := withGlobalGitConfigFile(tmpdir, ``) + require.NoError(t, err) + defer config() + _, recipeMetadata, _ := buildTestInputs() + if tt.desc == "invalid resource id" { + recipeMetadata.EnvironmentID = "//planes/radius/local/resourceGroups/r1/providers/Applications.Core/environments/env" + } + err = addSecretsToGitConfig(getSecretList(), &recipeMetadata, tt.templatePath) + if tt.expectedErr == nil { + require.NoError(t, err) + fileContent, err := os.ReadFile(filepath.Join(tmpdir, ".gitconfig")) + require.NoError(t, err) + require.Contains(t, string(fileContent), tt.expectedResponse) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedErr.Error()) + } + }) + } + +} +func TestUnsetConfig(t *testing.T) { + tests := []struct { + desc string + templatePath string + fileContent string + expectedResponse string + expectedErr error + }{ + { + desc: "success", + templatePath: "git::https://github.com/project/module", + fileContent: ` + [url "https://test-user:ghp_token@github.com"] + insteadOf = https://env1-app1-test-redis-recipe-github.com + `, + expectedErr: nil, + }, + { + desc: "invalid url", + templatePath: "git::https://git hub.com/project/module", + fileContent: ` + [url "https://test-user:ghp_token@github.com"] + insteadOf = https://env1-app1-test-redis-recipe-github.com + `, + expectedErr: errors.New("failed to parse git url"), + }, + { + desc: "empty config file", + templatePath: "git::https://github.com/project/module", + fileContent: "", + expectedErr: errors.New("failed to unset git config"), + }, + } + for _, tt := range tests { + tmpdir := t.TempDir() + config, err := withGlobalGitConfigFile(tmpdir, tt.fileContent) + require.NoError(t, err) + defer config() + err = unsetSecretsFromGitConfig(getSecretList(), tt.templatePath) + if tt.expectedErr == nil { + require.NoError(t, err) + contents, err := os.ReadFile(filepath.Join(tmpdir, ".gitconfig")) + require.NoError(t, err) + require.NotContains(t, string(contents), tt.fileContent) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedErr.Error()) + } + } +} + +func withGlobalGitConfigFile(tmpdir string, content string) (func(), error) { + + tmpGitConfigFile := filepath.Join(tmpdir, ".gitconfig") + + err := os.WriteFile( + tmpGitConfigFile, + []byte(content), + 0777, + ) + + if err != nil { + return func() {}, err + } + prevGitConfigEnv := os.Getenv("HOME") + os.Setenv("HOME", tmpdir) + + return func() { + os.Setenv("HOME", prevGitConfigEnv) + }, nil +} + +func getSecretList() v20231001preview.SecretStoresClientListSecretsResponse { + secrets := v20231001preview.SecretStoresClientListSecretsResponse{ + SecretStoreListSecretsResult: v20231001preview.SecretStoreListSecretsResult{ + Data: map[string]*v20231001preview.SecretValueProperties{ + "username": { + Value: to.Ptr("test-user"), + }, + "pat": { + Value: to.Ptr("ghp_token"), + }, + }, + }, + } + return secrets +} diff --git a/pkg/recipes/driver/terraform.go b/pkg/recipes/driver/terraform.go index b551495597..1800a2952a 100644 --- a/pkg/recipes/driver/terraform.go +++ b/pkg/recipes/driver/terraform.go @@ -22,10 +22,12 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "github.com/google/uuid" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" rpv1 "github.com/radius-project/radius/pkg/rp/v1" "golang.org/x/exp/slices" "k8s.io/client-go/kubernetes" @@ -90,12 +92,29 @@ func (d *terraformDriver) Execute(ctx context.Context, opts ExecuteOptions) (*re return nil, nil } + // Add credential information to .gitconfig if module source is of type git. + if strings.HasPrefix(opts.Definition.TemplatePath, "git::") && !reflect.DeepEqual(opts.BaseOptions.Secrets, v20231001preview.SecretStoresClientListSecretsResponse{}) { + err := addSecretsToGitConfig(opts.BaseOptions.Secrets, &opts.Recipe, opts.Definition.TemplatePath) + if err != nil { + return nil, err + } + } + tfState, err := d.terraformExecutor.Deploy(ctx, terraform.Options{ RootDir: requestDirPath, EnvConfig: &opts.Configuration, ResourceRecipe: &opts.Recipe, EnvRecipe: &opts.Definition, }) + + // Unset credential information from .gitconfig if module source is of type git. + if strings.HasPrefix(opts.Definition.TemplatePath, "git::") && !reflect.DeepEqual(opts.BaseOptions.Secrets, v20231001preview.SecretStoresClientListSecretsResponse{}) { + unsetError := unsetSecretsFromGitConfig(opts.BaseOptions.Secrets, opts.Definition.TemplatePath) + if unsetError != nil { + return nil, unsetError + } + } + if err != nil { return nil, recipes.NewRecipeError(recipes.RecipeDeploymentFailed, err.Error(), recipes_util.ExecutionError, recipes.GetErrorDetails(err)) } diff --git a/pkg/recipes/driver/types.go b/pkg/recipes/driver/types.go index ba9088768a..0b981d6827 100644 --- a/pkg/recipes/driver/types.go +++ b/pkg/recipes/driver/types.go @@ -19,6 +19,7 @@ package driver import ( "context" + "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" "github.com/radius-project/radius/pkg/recipes" rpv1 "github.com/radius-project/radius/pkg/rp/v1" ) @@ -51,6 +52,9 @@ type BaseOptions struct { // Definition is the environment definition for the recipe. Definition recipes.EnvironmentDefinition + + // Secrets specifies the module authentication information stored in the secret store. + Secrets v20231001preview.SecretStoresClientListSecretsResponse } // ExecuteOptions is the options for the Execute method. diff --git a/pkg/recipes/engine/engine.go b/pkg/recipes/engine/engine.go index c875cb8750..2571c2f2f9 100644 --- a/pkg/recipes/engine/engine.go +++ b/pkg/recipes/engine/engine.go @@ -21,6 +21,7 @@ import ( "fmt" "time" + "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" "github.com/radius-project/radius/pkg/metrics" "github.com/radius-project/radius/pkg/recipes" "github.com/radius-project/radius/pkg/recipes/configloader" @@ -39,6 +40,7 @@ var _ Engine = (*engine)(nil) // Options represents the configuration loader and type of driver used to deploy recipe. type Options struct { ConfigurationLoader configloader.ConfigurationLoader + SecretsLoader configloader.SecretsLoader Drivers map[string]recipedriver.Driver } @@ -81,11 +83,28 @@ func (e *engine) executeCore(ctx context.Context, recipe recipes.ResourceMetadat return nil, definition, recipes.NewRecipeError(recipes.RecipeConfigurationFailure, err.Error(), util.RecipeSetupError, recipes.GetErrorDetails(err)) } + // Retrieves the secret store id from the recipes configuration for the terraform module source of type git. + // secretStoreID returned will be an empty string for other types. + secretStore, err := recipes.GetSecretStoreID(*configuration, definition.TemplatePath) + if err != nil { + return nil, nil, err + } + + // Retrieves the secret values from the secret store ID provided. + secrets := v20231001preview.SecretStoresClientListSecretsResponse{} + if secretStore != "" { + secrets, err = e.options.SecretsLoader.LoadSecrets(ctx, secretStore) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch secrets from the secret store resource id %s for Terraform recipe %s deployment: %w", secretStore, definition.TemplatePath, err) + } + } + res, err := driver.Execute(ctx, recipedriver.ExecuteOptions{ BaseOptions: recipedriver.BaseOptions{ Configuration: *configuration, Recipe: recipe, Definition: *definition, + Secrets: secrets, }, PrevState: prevState, }) diff --git a/pkg/recipes/terraform/config/config.go b/pkg/recipes/terraform/config/config.go index 3d77abfa0a..30ff5691a4 100644 --- a/pkg/recipes/terraform/config/config.go +++ b/pkg/recipes/terraform/config/config.go @@ -23,6 +23,7 @@ import ( "fmt" "io/fs" "os" + "strings" "github.com/radius-project/radius/pkg/recipes" "github.com/radius-project/radius/pkg/recipes/recipecontext" @@ -38,10 +39,39 @@ const ( // New creates TerraformConfig with the given module name and its inputs (module source, version, parameters) // Parameters are populated from environment recipe and resource recipe metadata. -func New(moduleName string, envRecipe *recipes.EnvironmentDefinition, resourceRecipe *recipes.ResourceMetadata) *TerraformConfig { +func New(ctx context.Context, moduleName string, envRecipe *recipes.EnvironmentDefinition, resourceRecipe *recipes.ResourceMetadata, envConfig *recipes.Configuration) (*TerraformConfig, error) { + path := envRecipe.TemplatePath + + if envConfig != nil { + // Retrieving the secret store with associated with the template path. + // appends an URL prefix to the templatePath if secret store exists. + secretStore, err := recipes.GetSecretStoreID(*envConfig, envRecipe.TemplatePath) + if err != nil { + return nil, err + } + + if secretStore != "" { + // Retrieving the URL prefix, prefix will be in the format of https://--- + prefix, err := recipes.GetURLPrefix(resourceRecipe) + if err != nil { + return nil, err + } + + url, err := recipes.GetGitURL(envRecipe.TemplatePath) + if err != nil { + return nil, err + } + + // Adding URL prefix to the template path. + // Adding the prefix helps to access the the right credential information for git across environments. + // Updated template path will be added to the terraform config. + path = fmt.Sprintf("git::%s%s", prefix, strings.TrimPrefix(url.String(), "https://")) + } + } + // Resource parameter gets precedence over environment level parameter, // if same parameter is defined in both environment and resource recipe metadata. - moduleData := newModuleConfig(envRecipe.TemplatePath, envRecipe.TemplateVersion, envRecipe.Parameters, resourceRecipe.Parameters) + moduleData := newModuleConfig(path, envRecipe.TemplateVersion, envRecipe.Parameters, resourceRecipe.Parameters) return &TerraformConfig{ Terraform: nil, @@ -49,7 +79,7 @@ func New(moduleName string, envRecipe *recipes.EnvironmentDefinition, resourceRe Module: map[string]TFModuleConfig{ moduleName: moduleData, }, - } + }, nil } // getMainConfigFilePath returns the path of the Terraform main config file. diff --git a/pkg/recipes/terraform/config/config_test.go b/pkg/recipes/terraform/config/config_test.go index 17aebc0be3..a9f1f90174 100644 --- a/pkg/recipes/terraform/config/config_test.go +++ b/pkg/recipes/terraform/config/config_test.go @@ -17,6 +17,7 @@ limitations under the License. package config import ( + "context" "errors" "fmt" "os" @@ -116,6 +117,7 @@ func Test_NewConfig(t *testing.T) { desc string moduleName string envdef *recipes.EnvironmentDefinition + envConfig *recipes.Configuration metadata *recipes.ResourceMetadata expectedConfigFile string }{ @@ -173,16 +175,49 @@ func Test_NewConfig(t *testing.T) { }, expectedConfigFile: "testdata/module-emptytemplateversion.tf.json", }, + { + desc: "git private repo module", + moduleName: testRecipeName, + envdef: &recipes.EnvironmentDefinition{ + Name: testRecipeName, + TemplatePath: "git::https://dev.azure.com/project/module", + Parameters: envParams, + }, + envConfig: &recipes.Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Authentication: datamodel.AuthConfig{ + Git: datamodel.GitAuthConfig{ + PAT: map[string]datamodel.SecretConfig{ + "dev.azure.com": { + Secret: "secret-store1", + }, + }, + }, + }, + }, + }, + }, + metadata: &recipes.ResourceMetadata{ + Name: testRecipeName, + Parameters: resourceParams, + EnvironmentID: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Environments/testEnv/env", + ApplicationID: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Applications/testApp/app", + ResourceID: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Datastores/redisCaches/redis", + }, + expectedConfigFile: "testdata/module-private-git-repo.tf.json", + }, } for _, tc := range configTests { t.Run(tc.desc, func(t *testing.T) { workingDir := t.TempDir() - tfconfig := New(testRecipeName, tc.envdef, tc.metadata) + tfconfig, err := New(context.Background(), testRecipeName, tc.envdef, tc.metadata, tc.envConfig) + require.NoError(t, err) // validate generated config - err := tfconfig.Save(testcontext.New(t), workingDir) + err = tfconfig.Save(testcontext.New(t), workingDir) require.NoError(t, err) actualConfig, err := os.ReadFile(getMainConfigFilePath(workingDir)) require.NoError(t, err) @@ -272,9 +307,9 @@ func Test_AddRecipeContext(t *testing.T) { ctx := testcontext.New(t) workingDir := t.TempDir() - tfconfig := New(testRecipeName, tc.envdef, tc.metadata) - - err := tfconfig.AddRecipeContext(ctx, tc.moduleName, tc.recipeContext) + tfconfig, err := New(context.Background(), testRecipeName, tc.envdef, tc.metadata, nil) + require.NoError(t, err) + err = tfconfig.AddRecipeContext(ctx, tc.moduleName, tc.recipeContext) if tc.err == "" { require.NoError(t, err) } else { @@ -416,14 +451,15 @@ func Test_AddProviders(t *testing.T) { ctx := testcontext.New(t) workingDir := t.TempDir() - tfconfig := New(testRecipeName, &envRecipe, &resourceRecipe) + tfconfig, err := New(context.Background(), testRecipeName, &envRecipe, &resourceRecipe, nil) + require.NoError(t, err) for _, p := range tc.expectedProviders { mProvider.EXPECT().BuildConfig(ctx, &tc.envConfig).Times(1).Return(p, nil) } if tc.Err != nil { mProvider.EXPECT().BuildConfig(ctx, &tc.envConfig).Times(1).Return(nil, tc.Err) } - err := tfconfig.AddProviders(ctx, tc.requiredProviders, supportedProviders, &tc.envConfig) + err = tfconfig.AddProviders(ctx, tc.requiredProviders, supportedProviders, &tc.envConfig) if tc.Err != nil { require.ErrorContains(t, err, tc.Err.Error()) return @@ -476,9 +512,10 @@ func Test_AddOutputs(t *testing.T) { for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { - tfconfig := New(testRecipeName, &envRecipe, &resourceRecipe) + tfconfig, err := New(context.Background(), testRecipeName, &envRecipe, &resourceRecipe, nil) + require.NoError(t, err) - err := tfconfig.AddOutputs(tc.moduleName) + err = tfconfig.AddOutputs(tc.moduleName) if tc.expectedErr { require.Error(t, err) require.Nil(t, tfconfig.Output) @@ -505,9 +542,10 @@ func Test_Save_overwrite(t *testing.T) { ctx := testcontext.New(t) testDir := t.TempDir() envRecipe, resourceRecipe := getTestInputs() - tfconfig := New(testRecipeName, &envRecipe, &resourceRecipe) + tfconfig, err := New(context.Background(), testRecipeName, &envRecipe, &resourceRecipe, nil) + require.NoError(t, err) - err := tfconfig.Save(ctx, testDir) + err = tfconfig.Save(ctx, testDir) require.NoError(t, err) err = tfconfig.Save(ctx, testDir) @@ -517,10 +555,11 @@ func Test_Save_overwrite(t *testing.T) { func Test_Save_ConfigFileReadOnly(t *testing.T) { testDir := t.TempDir() envRecipe, resourceRecipe := getTestInputs() - tfconfig := New(testRecipeName, &envRecipe, &resourceRecipe) + tfconfig, err := New(context.Background(), testRecipeName, &envRecipe, &resourceRecipe, nil) + require.NoError(t, err) // Create a test configuration file with read only permission. - err := os.WriteFile(getMainConfigFilePath(testDir), []byte(`{"module":{}}`), 0400) + err = os.WriteFile(getMainConfigFilePath(testDir), []byte(`{"module":{}}`), 0400) require.NoError(t, err) // Assert that Save returns an error. @@ -533,9 +572,10 @@ func Test_Save_InvalidWorkingDir(t *testing.T) { testDir := filepath.Join("invalid", uuid.New().String()) envRecipe, resourceRecipe := getTestInputs() - tfconfig := New(testRecipeName, &envRecipe, &resourceRecipe) + tfconfig, err := New(context.Background(), testRecipeName, &envRecipe, &resourceRecipe, nil) + require.NoError(t, err) - err := tfconfig.Save(testcontext.New(t), testDir) + err = tfconfig.Save(testcontext.New(t), testDir) require.Error(t, err) require.Equal(t, fmt.Sprintf("error creating file: open %s/main.tf.json: no such file or directory", testDir), err.Error()) } diff --git a/pkg/recipes/terraform/config/testdata/module-private-git-repo.tf.json b/pkg/recipes/terraform/config/testdata/module-private-git-repo.tf.json new file mode 100644 index 0000000000..2f006e57ca --- /dev/null +++ b/pkg/recipes/terraform/config/testdata/module-private-git-repo.tf.json @@ -0,0 +1,11 @@ +{ + "terraform": null, + "module": { + "redis-azure": { + "redis_cache_name": "redis-test", + "resource_group_name": "test-rg", + "sku": "P", + "source": "git::https://env-app-redis-dev.azure.com/project/module" + } + } +} \ No newline at end of file diff --git a/pkg/recipes/terraform/execute.go b/pkg/recipes/terraform/execute.go index b10ec88fa4..248d4fa1c7 100644 --- a/pkg/recipes/terraform/execute.go +++ b/pkg/recipes/terraform/execute.go @@ -268,6 +268,7 @@ func downloadAndInspect(ctx context.Context, tf *tfexec.Terraform, options Optio // Download the Terraform module to the working directory. logger.Info(fmt.Sprintf("Downloading Terraform module: %s", options.EnvRecipe.TemplatePath)) downloadStartTime := time.Now() + if err := downloadModule(ctx, tf, options.EnvRecipe.TemplatePath); err != nil { metrics.DefaultRecipeEngineMetrics.RecordRecipeDownloadDuration(ctx, downloadStartTime, metrics.NewRecipeAttributes(metrics.RecipeEngineOperationDownloadRecipe, options.EnvRecipe.Name, @@ -302,7 +303,10 @@ func getTerraformConfig(ctx context.Context, workingDir string, options Options) } // Create Terraform configuration containing module information with the given recipe parameters. - tfConfig := config.New(localModuleName, options.EnvRecipe, options.ResourceRecipe) + tfConfig, err := config.New(ctx, localModuleName, options.EnvRecipe, options.ResourceRecipe, options.EnvConfig) + if err != nil { + return nil, err + } // Before downloading the module, Teraform configuration needs to be persisted in the working directory. // Terraform Get command uses this config file to download module from the source specified in the config. diff --git a/pkg/recipes/types.go b/pkg/recipes/types.go index d5245eaf5a..7b3da8892a 100644 --- a/pkg/recipes/types.go +++ b/pkg/recipes/types.go @@ -19,9 +19,13 @@ package recipes import ( "bytes" "encoding/json" + "fmt" + "net/url" + "strings" "github.com/radius-project/radius/pkg/corerp/datamodel" rpv1 "github.com/radius-project/radius/pkg/rp/v1" + "github.com/radius-project/radius/pkg/ucp/resources" ) // Configuration represents kubernetes runtime and cloud provider configuration, which is used by the driver while deploying recipes. @@ -32,6 +36,8 @@ type Configuration struct { Providers datamodel.Providers // Simulated represents whether the environment is simulated or not. Simulated bool + + RecipeConfig datamodel.RecipeConfigProperties } // RuntimeConfiguration represents Kubernetes Runtime configuration for the environment. @@ -135,3 +141,59 @@ func (ro *RecipeOutput) PrepareRecipeResponse(resultValue map[string]any) error return nil } + +// GetSecretStoreID returns secretstore resource ID associated with git private terraform repository source. +func GetSecretStoreID(envConfig Configuration, templatePath string) (string, error) { + if strings.HasPrefix(templatePath, "git::") { + url, err := GetGitURL(templatePath) + if err != nil { + return "", err + } + + // get the secret store id associated with the git domain of the template path. + return envConfig.RecipeConfig.Terraform.Authentication.Git.PAT[strings.TrimPrefix(url.Hostname(), "www.")].Secret, nil + } + return "", nil +} + +// GetGitURL returns git url from generic git module source. +// git::https://exmaple.com/project/module -> https://exmaple.com/project/module +func GetGitURL(templatePath string) (*url.URL, error) { + paths := strings.Split(templatePath, "git::") + gitUrl := paths[len(paths)-1] + url, err := url.Parse(gitUrl) + if err != nil { + return nil, fmt.Errorf("failed to parse git url %s : %w", gitUrl, err) + } + + return url, nil +} + +// GetEnvAppResourceNames returns the application, environment and resource names. +func GetEnvAppResourceNames(resourceMetadata *ResourceMetadata) (string, string, string, error) { + app, err := resources.ParseResource(resourceMetadata.ApplicationID) + if err != nil { + return "", "", "", err + } + + env, err := resources.ParseResource(resourceMetadata.EnvironmentID) + if err != nil { + return "", "", "", err + } + + resource, err := resources.ParseResource(resourceMetadata.ResourceID) + if err != nil { + return "", "", "", err + } + + return env.Name(), app.Name(), resource.Name(), nil +} + +// GetURLPrefix returns the url prefix to be added to the template path before adding it to the .gitconfig and terraform config. +func GetURLPrefix(resourceRecipe *ResourceMetadata) (string, error) { + env, app, resource, err := GetEnvAppResourceNames(resourceRecipe) + if err != nil { + return "", err + } + return fmt.Sprintf("https://%s-%s-%s-", env, app, resource), nil +} diff --git a/pkg/recipes/types_test.go b/pkg/recipes/types_test.go index 0535f6f060..38dfab59be 100644 --- a/pkg/recipes/types_test.go +++ b/pkg/recipes/types_test.go @@ -19,6 +19,7 @@ package recipes import ( "testing" + "github.com/radius-project/radius/pkg/corerp/datamodel" rpv1 "github.com/radius-project/radius/pkg/rp/v1" "github.com/stretchr/testify/require" ) @@ -80,3 +81,229 @@ func TestRecipeOutput_PrepareRecipeResponse(t *testing.T) { }) } } + +func Test_GetEnvAppResourceNames(t *testing.T) { + tests := []struct { + desc string + metadata ResourceMetadata + expApp string + expEnv string + expResource string + expectedErr bool + }{ + { + desc: "success", + metadata: ResourceMetadata{ + Name: "redis-azure", + ApplicationID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/applications/app1", + EnvironmentID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/environments/env1", + ResourceID: "/planes/radius/local/resourceGroups/test-rg/providers/applications.datastores/rediscaches/test-redis-recipe", + Parameters: map[string]any{ + "redis_cache_name": "redis-test", + }, + }, + expApp: "app1", + expEnv: "env1", + expResource: "test-redis-recipe", + expectedErr: false, + }, + { + desc: "invalid env id", + metadata: ResourceMetadata{ + Name: "redis-azure", + ApplicationID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/applications/app1", + EnvironmentID: "//planes/radius/local/resourcegroups/test-rg/providers/applications.core/environments/env1", + ResourceID: "/planes/radius/local/resourceGroups/test-rg/providers/applications.datastores/rediscaches/test-redis-recipe", + Parameters: map[string]any{ + "redis_cache_name": "redis-test", + }, + }, + expApp: "app1", + expEnv: "env1", + expResource: "test-redis-recipe", + expectedErr: true, + }, + { + desc: "invalid app id", + metadata: ResourceMetadata{ + Name: "redis-azure", + ApplicationID: "//planes/radius/local/resourcegroups/test-rg/providers/applications.core/applications/app1", + EnvironmentID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/environments/env1", + ResourceID: "/planes/radius/local/resourceGroups/test-rg/providers/applications.datastores/rediscaches/test-redis-recipe", + Parameters: map[string]any{ + "redis_cache_name": "redis-test", + }, + }, + expApp: "app1", + expEnv: "env1", + expResource: "test-redis-recipe", + expectedErr: true, + }, + { + desc: "invalid resource id", + metadata: ResourceMetadata{ + Name: "redis-azure", + ApplicationID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/applications/app1", + EnvironmentID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/environments/env1", + ResourceID: "//planes/radius/local/resourceGroups/test-rg/providers/applications.datastores/rediscaches/test-redis-recipe", + Parameters: map[string]any{ + "redis_cache_name": "redis-test", + }, + }, + expApp: "app1", + expEnv: "env1", + expResource: "test-redis-recipe", + expectedErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + env, app, res, err := GetEnvAppResourceNames(&tt.metadata) + if !tt.expectedErr { + require.Equal(t, tt.expApp, app) + require.Equal(t, tt.expEnv, env) + require.Equal(t, tt.expResource, res) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), "not a valid resource id") + } + }) + } +} + +func Test_GetGitURL(t *testing.T) { + tests := []struct { + desc string + templatePath string + expectedURL string + expectedErr bool + }{ + { + desc: "success", + templatePath: "git::https://dev.azure.com/project/module", + expectedURL: "https://dev.azure.com/project/module", + expectedErr: false, + }, + { + desc: "invalid url", + templatePath: "git::https://dev.az ure.com/project/module", + expectedErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + url, err := GetGitURL(tt.templatePath) + if !tt.expectedErr { + require.NoError(t, err) + require.Equal(t, tt.expectedURL, url.String()) + } else { + require.Error(t, err) + } + }) + } + +} + +func Test_GetSecretStoreID(t *testing.T) { + tests := []struct { + desc string + envConfig Configuration + templatePath string + expectedSecretStore string + expectedErr bool + }{ + { + desc: "success", + envConfig: Configuration{ + RecipeConfig: datamodel.RecipeConfigProperties{ + Terraform: datamodel.TerraformConfigProperties{ + Authentication: datamodel.AuthConfig{ + Git: datamodel.GitAuthConfig{ + PAT: map[string]datamodel.SecretConfig{ + "dev.azure.com": datamodel.SecretConfig{ + Secret: "secret-store1", + }, + }, + }, + }, + }, + }, + }, + templatePath: "git::https://dev.azure.com/project/module", + expectedSecretStore: "secret-store1", + expectedErr: false, + }, + { + desc: "empty config", + templatePath: "git::https://dev.azure.com/project/module", + expectedSecretStore: "", + expectedErr: false, + }, + { + desc: "invalid template path", + templatePath: "git::https://dev.azu re.com/project/module", + expectedSecretStore: "", + expectedErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ss, err := GetSecretStoreID(tt.envConfig, tt.templatePath) + if !tt.expectedErr { + require.NoError(t, err) + require.Equal(t, ss, tt.expectedSecretStore) + } else { + require.Error(t, err) + } + }) + } +} + +func Test_GetURLPrefix(t *testing.T) { + tests := []struct { + desc string + metadata ResourceMetadata + expectedPrefix string + expectedErr bool + }{ + { + desc: "success", + metadata: ResourceMetadata{ + Name: "redis-azure", + ApplicationID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/applications/app1", + EnvironmentID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/environments/env1", + ResourceID: "/planes/radius/local/resourceGroups/test-rg/providers/applications.datastores/rediscaches/redis", + Parameters: map[string]any{ + "redis_cache_name": "redis-test", + }, + }, + expectedPrefix: "https://env1-app1-redis-", + expectedErr: false, + }, + { + desc: "success", + metadata: ResourceMetadata{ + Name: "redis-azure", + ApplicationID: "//planes/radius/local/resourcegroups/test-rg/providers/applications.core/applications/app1", + EnvironmentID: "/planes/radius/local/resourcegroups/test-rg/providers/applications.core/environments/env1", + ResourceID: "/planes/radius/local/resourceGroups/test-rg/providers/applications.datastores/rediscaches/redis", + Parameters: map[string]any{ + "redis_cache_name": "redis-test", + }, + }, + expectedPrefix: "", + expectedErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ss, err := GetURLPrefix(&tt.metadata) + if !tt.expectedErr { + require.NoError(t, err) + require.Equal(t, ss, tt.expectedPrefix) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_CreateOrUpdate.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_CreateOrUpdate.json index 9998a5dd1f..6de0e8d45f 100644 --- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_CreateOrUpdate.json +++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_CreateOrUpdate.json @@ -17,6 +17,19 @@ "oidcIssuer": "https://oidcissuer/oidc" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_GetEnv0.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_GetEnv0.json index 8e7c297726..667d7ed7fe 100644 --- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_GetEnv0.json +++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_GetEnv0.json @@ -28,6 +28,19 @@ "scope":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_List.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_List.json index 4d7e7fc258..576046456f 100644 --- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_List.json +++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_List.json @@ -29,6 +29,19 @@ "scope":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_PatchEnv0.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_PatchEnv0.json index a193b84236..169fedb9f7 100644 --- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_PatchEnv0.json +++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/examples/Environments_PatchEnv0.json @@ -54,6 +54,19 @@ } } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "extensions": [ { "kind": "kubernetesMetadata", diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json index 83c5db3559..a884c7b418 100644 --- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json +++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2023-10-01-preview/openapi.json @@ -2740,6 +2740,16 @@ } } }, + "AuthConfig": { + "type": "object", + "description": "Authentication information used to access private Terraform module sources. Supported module sources: Git.", + "properties": { + "git": { + "$ref": "#/definitions/GitAuthConfig", + "description": "Authentication information used to access private Terraform modules from Git repository sources." + } + } + }, "AzureKeyVaultVolumeProperties": { "type": "object", "description": "Represents Azure Key Vault Volume properties", @@ -3492,6 +3502,10 @@ "type": "object" } }, + "recipeConfig": { + "$ref": "#/definitions/RecipeConfigProperties", + "description": "Configuration for Recipes. Defines how each type of Recipe should be configured and run." + }, "extensions": { "type": "array", "description": "The environment extension.", @@ -3592,6 +3606,10 @@ "type": "object" } }, + "recipeConfig": { + "$ref": "#/definitions/RecipeConfigProperties", + "description": "Configuration for Recipes. Defines how each type of Recipe should be configured and run." + }, "extensions": { "type": "array", "description": "The environment extension.", @@ -3996,6 +4014,19 @@ } } }, + "GitAuthConfig": { + "type": "object", + "description": "Authentication information used to access private Terraform modules from Git repository sources.", + "properties": { + "pat": { + "type": "object", + "description": "Personal Access Token (PAT) configuration used to authenticate to Git platforms.", + "additionalProperties": { + "$ref": "#/definitions/SecretConfig" + } + } + } + }, "HealthProbeProperties": { "type": "object", "description": "Properties for readiness/liveness probe", @@ -4717,6 +4748,16 @@ "name" ] }, + "RecipeConfigProperties": { + "type": "object", + "description": "Configuration for Recipes. Defines how each type of Recipe should be configured and run.", + "properties": { + "terraform": { + "$ref": "#/definitions/TerraformConfigProperties", + "description": "Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment." + } + } + }, "RecipeGetMetadata": { "type": "object", "description": "Represents the request body of the getmetadata action.", @@ -4951,6 +4992,16 @@ } } }, + "SecretConfig": { + "type": "object", + "description": "Personal Access Token (PAT) configuration used to authenticate to Git platforms.", + "properties": { + "secret": { + "type": "string", + "description": "The ID of an Applications.Core/SecretStore resource containing the Git platform personal access token (PAT). The secret store must have a secret named 'pat', containing the PAT value. A secret named 'username' is optional, containing the username associated with the pat. By default no username is specified." + } + } + }, "SecretObjectProperties": { "type": "object", "description": "Represents secret object properties", @@ -5221,6 +5272,16 @@ ], "x-ms-discriminator-value": "tcp" }, + "TerraformConfigProperties": { + "type": "object", + "description": "Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment.", + "properties": { + "authentication": { + "$ref": "#/definitions/AuthConfig", + "description": "Authentication information used to access private Terraform module sources. Supported module sources: Git." + } + } + }, "TerraformRecipeProperties": { "type": "object", "description": "Represents Terraform recipe properties.", diff --git a/typespec/Applications.Core/environments.tsp b/typespec/Applications.Core/environments.tsp index 8f721c88a3..9bb025b9bc 100644 --- a/typespec/Applications.Core/environments.tsp +++ b/typespec/Applications.Core/environments.tsp @@ -65,11 +65,44 @@ model EnvironmentProperties { @doc("Specifies Recipes linked to the Environment.") recipes?: Record>; + @doc("Configuration for Recipes. Defines how each type of Recipe should be configured and run.") + recipeConfig?: RecipeConfigProperties; + @doc("The environment extension.") @extension("x-ms-identifiers", []) extensions?: Array; } +@doc("Configuration for Recipes. Defines how each type of Recipe should be configured and run.") +model RecipeConfigProperties { + @doc("Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment.") + terraform?: TerraformConfigProperties; +} + +@doc("Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as part of Recipe deployment.") +model TerraformConfigProperties{ + @doc("Authentication information used to access private Terraform module sources. Supported module sources: Git.") + authentication?: AuthConfig; +} + +@doc("Authentication information used to access private Terraform module sources. Supported module sources: Git.") +model AuthConfig{ + @doc("Authentication information used to access private Terraform modules from Git repository sources.") + git?: GitAuthConfig; +} + +@doc("Authentication information used to access private Terraform modules from Git repository sources.") +model GitAuthConfig{ + @doc("Personal Access Token (PAT) configuration used to authenticate to Git platforms.") + pat?: Record; +} + +@doc("Personal Access Token (PAT) configuration used to authenticate to Git platforms.") +model SecretConfig { + @doc("The ID of an Applications.Core/SecretStore resource containing the Git platform personal access token (PAT). The secret store must have a secret named 'pat', containing the PAT value. A secret named 'username' is optional, containing the username associated with the pat. By default no username is specified.") + secret?: string; +} + @doc("The Cloud providers configuration") model Providers { @doc("The Azure cloud provider configuration") diff --git a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_CreateOrUpdate.json b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_CreateOrUpdate.json index 9998a5dd1f..6de0e8d45f 100644 --- a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_CreateOrUpdate.json +++ b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_CreateOrUpdate.json @@ -17,6 +17,19 @@ "oidcIssuer": "https://oidcissuer/oidc" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_GetEnv0.json b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_GetEnv0.json index 8e7c297726..667d7ed7fe 100644 --- a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_GetEnv0.json +++ b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_GetEnv0.json @@ -28,6 +28,19 @@ "scope":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_List.json b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_List.json index 4d7e7fc258..576046456f 100644 --- a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_List.json +++ b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_List.json @@ -29,6 +29,19 @@ "scope":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup" } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "recipes": { "Applications.Datastores/mongoDatabases":{ "cosmos-recipe": { diff --git a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_PatchEnv0.json b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_PatchEnv0.json index a193b84236..169fedb9f7 100644 --- a/typespec/Applications.Core/examples/2023-10-01-preview/Environments_PatchEnv0.json +++ b/typespec/Applications.Core/examples/2023-10-01-preview/Environments_PatchEnv0.json @@ -54,6 +54,19 @@ } } }, + "recipeConfig": { + "terraform": { + "authentication": { + "git": { + "pat": { + "dev.azure.com":{ + "secret":"/planes/radius/local/resourcegroups/default/providers/Applications.Core/secretStores/github" + } + } + } + } + } + }, "extensions": [ { "kind": "kubernetesMetadata",