diff --git a/.github/workflows/functional-test.yaml b/.github/workflows/functional-test.yaml index 6231921e7d..bcf12e2b91 100644 --- a/.github/workflows/functional-test.yaml +++ b/.github/workflows/functional-test.yaml @@ -521,7 +521,7 @@ jobs: echo "*** Configuring Azure provider ***" rad env update kind-radius --azure-subscription-id ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} \ --azure-resource-group ${{ env.AZURE_TEST_RESOURCE_GROUP }} - rad credential register azure --client-id ${{ secrets.AZURE_SP_TESTS_APPID }} \ + rad credential register azure sp --client-id ${{ secrets.AZURE_SP_TESTS_APPID }} \ --client-secret ${{ secrets.INTEGRATION_TEST_SP_PASSWORD }} \ --tenant-id ${{ secrets.AZURE_SP_TESTS_TENANTID }} diff --git a/.github/workflows/long-running-azure.yaml b/.github/workflows/long-running-azure.yaml index 0568736b05..b1f242ce30 100644 --- a/.github/workflows/long-running-azure.yaml +++ b/.github/workflows/long-running-azure.yaml @@ -423,7 +423,7 @@ jobs: echo "*** Configuring Azure provider ***" rad env update ${{ env.RADIUS_TEST_ENVIRONMENT_NAME }} --azure-subscription-id ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }} \ --azure-resource-group ${{ env.AZURE_TEST_RESOURCE_GROUP }} - rad credential register azure --client-id ${{ secrets.AZURE_SP_TESTS_APPID }} \ + rad credential register azure sp --client-id ${{ secrets.AZURE_SP_TESTS_APPID }} \ --client-secret ${{ secrets.INTEGRATION_TEST_SP_PASSWORD }} \ --tenant-id ${{ secrets.AZURE_SP_TESTS_TENANTID }} diff --git a/deploy/Chart/templates/de/deployment.yaml b/deploy/Chart/templates/de/deployment.yaml index 11bf4c135e..85eab2efcb 100644 --- a/deploy/Chart/templates/de/deployment.yaml +++ b/deploy/Chart/templates/de/deployment.yaml @@ -19,6 +19,9 @@ spec: control-plane: bicep-de app.kubernetes.io/name: bicep-de app.kubernetes.io/part-of: radius + {{- if eq .Values.global.azureWorkloadIdentity.enabled true }} + azure.workload.identity/use: "true" + {{- end }} {{- if eq .Values.global.prometheus.enabled true }} annotations: prometheus.io/path: "/metrics" diff --git a/deploy/Chart/templates/rp/deployment.yaml b/deploy/Chart/templates/rp/deployment.yaml index d244a43427..c82fba9016 100644 --- a/deploy/Chart/templates/rp/deployment.yaml +++ b/deploy/Chart/templates/rp/deployment.yaml @@ -19,6 +19,9 @@ spec: control-plane: applications-rp app.kubernetes.io/name: applications-rp app.kubernetes.io/part-of: radius + {{- if eq .Values.global.azureWorkloadIdentity.enabled true }} + azure.workload.identity/use: "true" + {{- end }} {{- if eq .Values.global.prometheus.enabled true }} annotations: prometheus.io/path: "{{ .Values.global.prometheus.path }}" diff --git a/deploy/Chart/templates/ucp/deployment.yaml b/deploy/Chart/templates/ucp/deployment.yaml index b210149850..4c497fd8e6 100644 --- a/deploy/Chart/templates/ucp/deployment.yaml +++ b/deploy/Chart/templates/ucp/deployment.yaml @@ -19,6 +19,9 @@ spec: control-plane: ucp app.kubernetes.io/name: ucp app.kubernetes.io/part-of: radius + {{- if eq .Values.global.azureWorkloadIdentity.enabled true }} + azure.workload.identity/use: "true" + {{- end }} {{- if eq .Values.global.prometheus.enabled true }} annotations: prometheus.io/path: "{{ .Values.global.prometheus.path }}" diff --git a/deploy/Chart/values.yaml b/deploy/Chart/values.yaml index 1d9df6fa5e..359aff7f19 100644 --- a/deploy/Chart/values.yaml +++ b/deploy/Chart/values.yaml @@ -21,6 +21,11 @@ global: # url: "http://jaeger-collector.radius-monitoring.svc.cluster.local:9411/api/v2/spans" # + # Configure global.azureWorkloadIdentity.enabled=true to enable Azure Workload Identity. + # Disabled by default. + azureWorkloadIdentity: + enabled: false + controller: image: ghcr.io/radius-project/controller # Default tag uses Chart AppVersion. diff --git a/docs/contributing/contributing-code/contributing-code-tests/testing-local.md b/docs/contributing/contributing-code/contributing-code-tests/testing-local.md index 0980c3756a..a362ca5284 100644 --- a/docs/contributing/contributing-code/contributing-code-tests/testing-local.md +++ b/docs/contributing/contributing-code/contributing-code-tests/testing-local.md @@ -55,7 +55,7 @@ The above steps will not configure the ability for Radius to talk with azure res go run ./cmd/rad/main.go env create radius-rg --namespace default go run ./cmd/rad/main.go env switch radius-rg go run ./cmd/rad/main.go env update radius-rg --azure-subscription-id --azure-resource-group - go run ./cmd/rad/main.go credential register azure --client-id --client-secret --tenant-id + go run ./cmd/rad/main.go credential register azure sp --client-id --client-secret --tenant-id ``` 1. Run a deployment. Executing `go run \cmd\rad\main.go deploy ` will deploy your file to the cluster. diff --git a/pkg/azure/armauth/auth.go b/pkg/azure/armauth/auth.go index 1a01674e79..940178fd1d 100644 --- a/pkg/azure/armauth/auth.go +++ b/pkg/azure/armauth/auth.go @@ -30,6 +30,7 @@ import ( const ( UCPCredentialAuth = "UCPCredential" ServicePrincipalAuth = "ServicePrincipal" + WorkloadIdentityAuth = "WorkloadIdentity" ManagedIdentityAuth = "ManagedIdentity" CliAuth = "CLI" ) @@ -67,6 +68,8 @@ func NewArmConfig(opt *Options) (*ArmConfig, error) { func NewARMCredential(opt *Options) (azcore.TokenCredential, error) { authMethod := GetAuthMethod() + // Use the Azure SDK for Go to create a credential based on the authentication method + // https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview?tabs=go#azure-identity-client-libraries switch authMethod { case UCPCredentialAuth: return azcred.NewUCPCredential(azcred.UCPCredentialOptions{ @@ -76,6 +79,8 @@ func NewARMCredential(opt *Options) (azcore.TokenCredential, error) { return azidentity.NewEnvironmentCredential(nil) case ManagedIdentityAuth: return azidentity.NewManagedIdentityCredential(nil) + case WorkloadIdentityAuth: + return azidentity.NewDefaultAzureCredential(nil) default: return azidentity.NewAzureCLICredential(nil) } @@ -101,8 +106,3 @@ func GetAuthMethod() string { return CliAuth } } - -// IsServicePrincipalConfigured checks if ServicePrincipalAuth is the authentication method configured. -func IsServicePrincipalConfigured() (bool, error) { - return GetAuthMethod() == ServicePrincipalAuth, nil -} diff --git a/pkg/azure/credential/ucpcredentials.go b/pkg/azure/credential/ucpcredentials.go index b98eb5baf5..8f80bcf8b7 100644 --- a/pkg/azure/credential/ucpcredentials.go +++ b/pkg/azure/credential/ucpcredentials.go @@ -19,6 +19,7 @@ package credential import ( "context" "errors" + "fmt" "sync" "time" @@ -26,6 +27,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" sdk_cred "github.com/radius-project/radius/pkg/ucp/credentials" + "github.com/radius-project/radius/pkg/ucp/datamodel" "github.com/radius-project/radius/pkg/ucp/ucplog" "go.uber.org/atomic" @@ -87,8 +89,6 @@ func (c *UCPCredential) refreshExpiry() { } func (c *UCPCredential) refreshCredentials(ctx context.Context) error { - logger := ucplog.FromContextOrDiscard(ctx) - c.tokenCredMu.Lock() defer c.tokenCredMu.Unlock() @@ -102,18 +102,35 @@ func (c *UCPCredential) refreshCredentials(ctx context.Context) error { return err } - if s.ClientID == "" || s.ClientSecret == "" || s.TenantID == "" { - return errors.New("invalid azure service principal credential info") + switch s.Kind { + case datamodel.AzureServicePrincipalCredentialKind: + return refreshAzureServicePrincipalCredentials(ctx, c, s) + case datamodel.AzureWorkloadIdentityCredentialKind: + return refreshAzureWorkloadIdentityCredentials(ctx, c, s) + default: + return fmt.Errorf("unknown Azure credential kind, expected ServicePrincipal or WorkloadIdentity (got %s)", s.Kind) + } +} + +func refreshAzureServicePrincipalCredentials(ctx context.Context, c *UCPCredential, s *sdk_cred.AzureCredential) error { + logger := ucplog.FromContextOrDiscard(ctx) + + azureServicePrincipalCredential := s.ServicePrincipal + if azureServicePrincipalCredential.ClientID == "" || azureServicePrincipalCredential.ClientSecret == "" || azureServicePrincipalCredential.TenantID == "" { + return errors.New("Client ID, Tenant ID, or Client Secret can't be empty") } // Do not instantiate new client unless the secret is rotated. - if c.credential != nil && c.credential.ClientSecret == s.ClientSecret && - c.credential.ClientID == s.ClientID && c.credential.TenantID == s.TenantID { + if c.credential != nil && + c.credential.ServicePrincipal != nil && + c.credential.ServicePrincipal.ClientSecret == azureServicePrincipalCredential.ClientSecret && + c.credential.ServicePrincipal.ClientID == azureServicePrincipalCredential.ClientID && + c.credential.ServicePrincipal.TenantID == azureServicePrincipalCredential.TenantID { c.refreshExpiry() return nil } - logger.Info("Retreived Azure Credential - ClientID: " + s.ClientID) + logger.Info("Retrieved Azure Credential - ClientID: " + azureServicePrincipalCredential.ClientID) // Rotate credentials by creating new ClientSecretCredential. var opt *azidentity.ClientSecretCredentialOptions @@ -123,7 +140,45 @@ func (c *UCPCredential) refreshCredentials(ctx context.Context) error { } } - azCred, err := azidentity.NewClientSecretCredential(s.TenantID, s.ClientID, s.ClientSecret, opt) + azCred, err := azidentity.NewClientSecretCredential(azureServicePrincipalCredential.TenantID, azureServicePrincipalCredential.ClientID, azureServicePrincipalCredential.ClientSecret, opt) + if err != nil { + return err + } + + c.tokenCred = azCred + c.credential = s + + c.refreshExpiry() + return nil +} + +func refreshAzureWorkloadIdentityCredentials(ctx context.Context, c *UCPCredential, s *sdk_cred.AzureCredential) error { + logger := ucplog.FromContextOrDiscard(ctx) + + azureWorkloadIdentityCredential := s.WorkloadIdentity + if azureWorkloadIdentityCredential.ClientID == "" || azureWorkloadIdentityCredential.TenantID == "" { + return errors.New("empty clientID or tenantID provided for Azure workload identity") + } + + // Do not instantiate new client unless clientId and tenantId are changed. + if c.credential != nil && + c.credential.WorkloadIdentity != nil && + c.credential.WorkloadIdentity.ClientID == azureWorkloadIdentityCredential.ClientID && + c.credential.WorkloadIdentity.TenantID == azureWorkloadIdentityCredential.TenantID { + c.refreshExpiry() + return nil + } + + logger.Info("Retrieved Azure Credential - ClientID: " + azureWorkloadIdentityCredential.ClientID) + + var opt *azidentity.DefaultAzureCredentialOptions + if c.options.ClientOptions != nil { + opt = &azidentity.DefaultAzureCredentialOptions{ + ClientOptions: *c.options.ClientOptions, + } + } + + azCred, err := azidentity.NewDefaultAzureCredential(opt) if err != nil { return err } @@ -135,7 +190,7 @@ func (c *UCPCredential) refreshCredentials(ctx context.Context) error { return nil } -// GetToken attempts to refresh the Azure service principal credential if it is expired and then returns an +// GetToken attempts to refresh the Azure credential if it is expired and then returns an // access token if the credential is ready. This method is called automatically by Azure SDK clients. func (c *UCPCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { logger := ucplog.FromContextOrDiscard(ctx) @@ -143,7 +198,7 @@ func (c *UCPCredential) GetToken(ctx context.Context, opts policy.TokenRequestOp if c.isExpired() { err := c.refreshCredentials(ctx) if err != nil { - logger.Error(err, "failed to refresh Azure service principal credential.") + logger.Error(err, "failed to refresh Azure credential.") } } @@ -152,7 +207,7 @@ func (c *UCPCredential) GetToken(ctx context.Context, opts policy.TokenRequestOp c.tokenCredMu.RUnlock() if credentialAuth == nil { - return azcore.AccessToken{}, errors.New("azure service principal credential is not ready") + return azcore.AccessToken{}, errors.New("azure credential is not ready") } return credentialAuth.GetToken(ctx, opts) diff --git a/pkg/azure/credential/ucpcredentials_test.go b/pkg/azure/credential/ucpcredentials_test.go index 7a94f58412..907aea6fe0 100644 --- a/pkg/azure/credential/ucpcredentials_test.go +++ b/pkg/azure/credential/ucpcredentials_test.go @@ -38,39 +38,105 @@ func (p *mockProvider) Fetch(ctx context.Context, planeName, name string) (*sdk_ return p.fakeCredential, nil } -func newMockProvider() *mockProvider { +func newServicePrincipalMockProvider() *mockProvider { return &mockProvider{ fakeCredential: &sdk_cred.AzureCredential{ - ClientID: "fakeid", - TenantID: "fakeid", - ClientSecret: "fakeSecret", + Kind: sdk_cred.AzureServicePrincipalCredentialKind, + ServicePrincipal: &sdk_cred.AzureServicePrincipalCredential{ + ClientID: "fakeid", + TenantID: "fakeid", + ClientSecret: "fakeSecret", + }, }, } } -func TestNewUCPCredential(t *testing.T) { +func newWorkloadIdentityMockProvider() *mockProvider { + return &mockProvider{ + fakeCredential: &sdk_cred.AzureCredential{ + Kind: sdk_cred.AzureWorkloadIdentityCredentialKind, + WorkloadIdentity: &sdk_cred.AzureWorkloadIdentityCredential{ + ClientID: "fakeid", + TenantID: "fakeid", + }, + }, + } +} + +func Test_NewUCPCredential_AzureServicePrincipal(t *testing.T) { _, err := NewUCPCredential(UCPCredentialOptions{}) require.Error(t, err) - c, err := NewUCPCredential(UCPCredentialOptions{Provider: newMockProvider()}) + c, err := NewUCPCredential(UCPCredentialOptions{Provider: newServicePrincipalMockProvider()}) require.NoError(t, err) require.Equal(t, DefaultExpireDuration, c.options.Duration) require.True(t, c.isExpired()) } -func TestRefreshCredentials(t *testing.T) { +func Test_NewUCPCredential_WorkloadIdentity(t *testing.T) { + _, err := NewUCPCredential(UCPCredentialOptions{}) + require.Error(t, err) + + c, err := NewUCPCredential(UCPCredentialOptions{Provider: newWorkloadIdentityMockProvider()}) + require.NoError(t, err) + require.Equal(t, DefaultExpireDuration, c.options.Duration) + require.True(t, c.isExpired()) +} + +func Test_RefreshCredentials_ServicePrincipal(t *testing.T) { + t.Run("invalid credential", func(t *testing.T) { + p := newServicePrincipalMockProvider() + c, err := NewUCPCredential(UCPCredentialOptions{Provider: p}) + require.NoError(t, err) + p.fakeCredential.ServicePrincipal.ClientID = "" + + err = c.refreshCredentials(context.TODO()) + require.Error(t, err) + }) + + t.Run("do not refresh credential", func(t *testing.T) { + p := newServicePrincipalMockProvider() + c, err := NewUCPCredential(UCPCredentialOptions{Provider: p}) + require.NoError(t, err) + + err = c.refreshCredentials(context.TODO()) + require.NoError(t, err) + require.False(t, c.isExpired()) + }) + + t.Run("same credentials", func(t *testing.T) { + p := newServicePrincipalMockProvider() + c, err := NewUCPCredential(UCPCredentialOptions{Provider: p}) + require.NoError(t, err) + + err = c.refreshCredentials(context.TODO()) + require.NoError(t, err) + + // reset next refresh time. + c.nextExpiry.Store(0) + require.True(t, c.isExpired()) + old := c.tokenCred + + err = c.refreshCredentials(context.TODO()) + require.NoError(t, err) + require.False(t, c.isExpired()) + require.Equal(t, old, c.tokenCred) + }) +} + +func Test_RefreshCredentials_WorkloadIdentity(t *testing.T) { t.Run("invalid credential", func(t *testing.T) { - p := newMockProvider() + p := newWorkloadIdentityMockProvider() c, err := NewUCPCredential(UCPCredentialOptions{Provider: p}) require.NoError(t, err) - p.fakeCredential.ClientID = "" + p.fakeCredential.WorkloadIdentity.ClientID = "" err = c.refreshCredentials(context.TODO()) require.Error(t, err) }) t.Run("do not refresh credential", func(t *testing.T) { - p := newMockProvider() + p := newWorkloadIdentityMockProvider() c, err := NewUCPCredential(UCPCredentialOptions{Provider: p}) require.NoError(t, err) @@ -80,7 +146,7 @@ func TestRefreshCredentials(t *testing.T) { }) t.Run("same credentials", func(t *testing.T) { - p := newMockProvider() + p := newWorkloadIdentityMockProvider() c, err := NewUCPCredential(UCPCredentialOptions{Provider: p}) require.NoError(t, err) diff --git a/pkg/cli/azure/provider.go b/pkg/cli/azure/provider.go index 6a0382b7b4..8a60c14ddb 100644 --- a/pkg/cli/azure/provider.go +++ b/pkg/cli/azure/provider.go @@ -16,20 +16,33 @@ limitations under the License. package azure +// AzureCredentialKind - Azure credential kinds supported. +type AzureCredentialKind string + const ( // ProviderDisplayName is the text used in display for Azure. - ProviderDisplayName = "Azure" + ProviderDisplayName = "Azure" + AzureCredentialKindWorkloadIdentity AzureCredentialKind = "WorkloadIdentity" + AzureCredentialKindServicePrincipal AzureCredentialKind = "ServicePrincipal" ) // Provider specifies the properties required to configure Azure provider for cloud resources type Provider struct { SubscriptionID string ResourceGroup string - ServicePrincipal *ServicePrincipal + CredentialKind AzureCredentialKind + WorkloadIdentity *WorkloadIdentityCredential + ServicePrincipal *ServicePrincipalCredential +} + +// Wor specifies the properties of an Azure service principal +type WorkloadIdentityCredential struct { + ClientID string + TenantID string } // ServicePrincipal specifies the properties of an Azure service principal -type ServicePrincipal struct { +type ServicePrincipalCredential struct { ClientID string ClientSecret string TenantID string diff --git a/pkg/cli/cmd/credential/credential.go b/pkg/cli/cmd/credential/credential.go index bb58952131..a019e536cb 100644 --- a/pkg/cli/cmd/credential/credential.go +++ b/pkg/cli/cmd/credential/credential.go @@ -42,7 +42,9 @@ func NewCommand(factory framework.Factory) *cobra.Command { rad credential list # Register (Add or Update) cloud provider credential for Azure with service principal authentication -rad credential register azure --client-id --client-secret --tenant-id +rad credential register azure sp --client-id --client-secret --tenant-id +# Register (Add or Update) cloud provider credential for Azure with workload identity authentication +rad credential register azure wi --client-id --tenant-id # Register (Add or Update) cloud provider credential for AWS with IAM authentication rad credential register aws --access-key-id --secret-access-key diff --git a/pkg/cli/cmd/credential/register/azure/azure.go b/pkg/cli/cmd/credential/register/azure/azure.go index f668737419..3fe54f0ede 100644 --- a/pkg/cli/cmd/credential/register/azure/azure.go +++ b/pkg/cli/cmd/credential/register/azure/azure.go @@ -17,173 +17,34 @@ limitations under the License. package azure import ( - "context" - "fmt" - - v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" - "github.com/radius-project/radius/pkg/cli" - "github.com/radius-project/radius/pkg/cli/clierrors" - "github.com/radius-project/radius/pkg/cli/cmd/commonflags" "github.com/radius-project/radius/pkg/cli/cmd/credential/common" - "github.com/radius-project/radius/pkg/cli/connections" - cli_credential "github.com/radius-project/radius/pkg/cli/credential" + azuresp "github.com/radius-project/radius/pkg/cli/cmd/credential/register/azure/sp" + azurewi "github.com/radius-project/radius/pkg/cli/cmd/credential/register/azure/wi" "github.com/radius-project/radius/pkg/cli/framework" - "github.com/radius-project/radius/pkg/cli/output" - "github.com/radius-project/radius/pkg/cli/workspaces" - "github.com/radius-project/radius/pkg/to" - ucp "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" - "github.com/spf13/cobra" ) -// NewCommand creates an instance of the command and runner for the `rad credential create azure` command. -// - -// NewCommand creates a new cobra command for registering an Azure cloud provider credential for a Radius installation, -// which requires a service principal with the Contributor or Owner role assigned to the provided resource group. -func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { - runner := NewRunner(factory) - +// NewCommand creates an instance of the command for the `rad credential register azure` command. +// This command is not runnable, but contains subcommands for registering Azure cloud provider credentials. +func NewCommand(factory framework.Factory) *cobra.Command { + // This command is not runnable, and thus has no runner. cmd := &cobra.Command{ Use: "azure", Short: "Register (Add or update) Azure cloud provider credential for a Radius installation.", - Long: `Register (Add or update) Azure cloud provider credential for a Radius installation.. - -This command is intended for scripting or advanced use-cases. See 'rad init' for a user-friendly way -to configure these settings. - -Radius will use the provided service principal for all interactions with Azure, including Bicep deployment, -Radius Environments, and Radius portable resources. - -Radius will use the provided subscription and resource group as the default target scope for Bicep deployment. -The provided service principal must have the Contributor or Owner role assigned for the provided resource group -in order to create or manage resources contained in the group. The resource group should be created before -calling 'rad credential register azure'. -` + common.LongDescriptionBlurb, + Long: "Register (Add or update) Azure cloud provider credential for a Radius installation." + common.LongDescriptionBlurb, Example: ` # Register (Add or update) cloud provider credential for Azure with service principal authentication -rad credential register azure --client-id --client-secret --tenant-id +rad credential register azure sp --client-id --client-secret --tenant-id +# Register (Add or update) cloud provider credential for Azure with workload identity authentication +rad credential register azure wi --client-id --tenant-id `, - Args: cobra.ExactArgs(0), - RunE: framework.RunCommand(runner), - } - - commonflags.AddOutputFlag(cmd) - commonflags.AddWorkspaceFlag(cmd) - - cmd.Flags().String("client-id", "", "The client id or app id of an Azure service principal.") - _ = cmd.MarkFlagRequired("client-id") - - cmd.Flags().String("client-secret", "", "The client secret or password of an Azure service principal.") - _ = cmd.MarkFlagRequired("client-secret") - - cmd.Flags().String("tenant-id", "", "The tenant id of an Azure service principal.") - _ = cmd.MarkFlagRequired("tenant-id") - - return cmd, runner -} - -// Runner is the runner implementation for the `rad credential register azure` command. -type Runner struct { - ConfigHolder *framework.ConfigHolder - ConnectionFactory connections.Factory - Output output.Interface - Format string - Workspace *workspaces.Workspace - - ClientID string - ClientSecret string - TenantID string - KubeContext string -} - -// NewRunner creates a new instance of the `rad credential register azure` runner. -func NewRunner(factory framework.Factory) *Runner { - return &Runner{ - ConfigHolder: factory.GetConfigHolder(), - ConnectionFactory: factory.GetConnectionFactory(), - Output: factory.GetOutput(), } -} - -// Validate runs validation for the `rad credential register azure` command. -// -// Validate checks for the presence of a workspace, output format, client ID, client secret and tenant ID, and -// sets them in the Runner struct if they are present. If any of these are not present, an error is returned. -func (r *Runner) Validate(cmd *cobra.Command, args []string) error { - workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) - if err != nil { - return err - } - r.Workspace = workspace - - format, err := cli.RequireOutput(cmd) - if err != nil { - return err - } - r.Format = format - - clientID, err := cmd.Flags().GetString("client-id") - if err != nil { - return err - } - clientSecret, err := cmd.Flags().GetString("client-secret") - if err != nil { - return err - } - tenantID, err := cmd.Flags().GetString("tenant-id") - if err != nil { - return err - } - - r.ClientID = clientID - r.ClientSecret = clientSecret - r.TenantID = tenantID - - kubeContext, ok := r.Workspace.KubernetesContext() - if !ok { - return clierrors.Message("A Kubernetes connection is required.") - } - r.KubeContext = kubeContext - - return nil -} - -// Run runs the `rad credential register azure` command. -// - -// Run registers a credential for the Azure cloud provider in the Radius installation, updates the server-side -// to add/change credentials. It returns an error if any of the steps fail. -func (r *Runner) Run(ctx context.Context) error { - r.Output.LogInfo("Registering credential for %q cloud provider in Radius installation %q...", "azure", r.Workspace.FmtConnection()) - client, err := r.ConnectionFactory.CreateCredentialManagementClient(ctx, *r.Workspace) - if err != nil { - return err - } - - credential := ucp.AzureCredentialResource{ - Location: to.Ptr(v1.LocationGlobal), - Type: to.Ptr(cli_credential.AzureCredential), - ID: to.Ptr(fmt.Sprintf(common.AzureCredentialID, "default")), - Properties: &ucp.AzureServicePrincipalProperties{ - Storage: &ucp.CredentialStorageProperties{ - Kind: to.Ptr(ucp.CredentialStorageKindInternal), - }, - TenantID: &r.TenantID, - ClientID: &r.ClientID, - ClientSecret: &r.ClientSecret, - Kind: to.Ptr(ucp.AzureCredentialKindServicePrincipal), - }, - } - - // Update server-side to add/change credentials - err = client.PutAzure(ctx, credential) - if err != nil { - return err - } + azureSP, _ := azuresp.NewCommand(factory) + cmd.AddCommand(azureSP) - r.Output.LogInfo("Successfully registered credential for %q cloud provider. Tokens may take up to 30 seconds to refresh.", "azure") + azureWI, _ := azurewi.NewCommand(factory) + cmd.AddCommand(azureWI) - return nil + return cmd } diff --git a/pkg/cli/cmd/credential/register/azure/sp/serviceprincipal.go b/pkg/cli/cmd/credential/register/azure/sp/serviceprincipal.go new file mode 100644 index 0000000000..6f41bff3ad --- /dev/null +++ b/pkg/cli/cmd/credential/register/azure/sp/serviceprincipal.go @@ -0,0 +1,161 @@ +/* +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 sp + +import ( + "context" + "fmt" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/credential/common" + "github.com/radius-project/radius/pkg/cli/connections" + cli_credential "github.com/radius-project/radius/pkg/cli/credential" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" + ucp "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the command and runner for the `rad credential register azure sp` command. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "sp", + Short: "Register (Add or update) Azure cloud provider service principal credential for a Radius installation.", + Long: `Register (Add or update) Azure cloud provider service principal credential for a Radius installation. + +This command is intended for scripting or advanced use-cases. See 'rad init' for a user-friendly way +to configure these settings. + +Radius will use the provided service principal for all interactions with Azure, including Bicep deployment, +Radius Environments, and Radius portable resources. + +Radius will use the provided subscription and resource group as the default target scope for Bicep deployment. +The provided service principal must have the Contributor or Owner role assigned for the provided resource group +in order to create or manage resources contained in the group. The resource group should be created before +calling 'rad credential register azure sp'. +` + common.LongDescriptionBlurb, + Example: ` +# Register (Add or update) cloud provider credential for Azure with service principal authentication +rad credential register azure sp --client-id --client-secret --tenant-id +`, + Args: cobra.ExactArgs(0), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + cmd.Flags().StringVar(&runner.ClientID, "client-id", "", "The client id or app id of an Azure service principal.") + _ = cmd.MarkFlagRequired("client-id") + + cmd.Flags().StringVar(&runner.ClientSecret, "client-secret", "", "The client secret or password of an Azure service principal.") + _ = cmd.MarkFlagRequired("client-secret") + + cmd.Flags().StringVar(&runner.TenantID, "tenant-id", "", "The tenant id of an Azure service principal.") + _ = cmd.MarkFlagRequired("tenant-id") + + return cmd, runner +} + +// Runner is the runner implementation for the `rad credential register azure sp` command. +type Runner struct { + ConfigHolder *framework.ConfigHolder + ConnectionFactory connections.Factory + Output output.Interface + Format string + Workspace *workspaces.Workspace + + ClientID string + ClientSecret string + TenantID string + KubeContext string +} + +// NewRunner creates a new instance of the `rad credential register azure sp` runner. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConfigHolder: factory.GetConfigHolder(), + ConnectionFactory: factory.GetConnectionFactory(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad credential register azure sp` command. +// + +// Validate checks for the presence of a workspace, output format, client ID, client secret and tenant ID, and +// sets them in the Runner struct if they are present. If any of these are not present, an error is returned. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + return nil +} + +// Run runs the `rad credential register azure sp` command. +// + +// Run registers a credential for the Azure cloud provider in the Radius installation, updates the server-side +// to add/change credentials. It returns an error if any of the steps fail. +func (r *Runner) Run(ctx context.Context) error { + r.Output.LogInfo("Registering credential for %q cloud provider in Radius installation %q...", "azure", r.Workspace.FmtConnection()) + client, err := r.ConnectionFactory.CreateCredentialManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + credential := ucp.AzureCredentialResource{ + Location: to.Ptr(v1.LocationGlobal), + Type: to.Ptr(cli_credential.AzureCredential), + ID: to.Ptr(fmt.Sprintf(common.AzureCredentialID, "default")), + Properties: &ucp.AzureServicePrincipalProperties{ + Storage: &ucp.CredentialStorageProperties{ + Kind: to.Ptr(ucp.CredentialStorageKindInternal), + }, + TenantID: &r.TenantID, + ClientID: &r.ClientID, + ClientSecret: &r.ClientSecret, + Kind: to.Ptr(ucp.AzureCredentialKindServicePrincipal), + }, + } + + // Update server-side to add/change credentials + err = client.PutAzure(ctx, credential) + if err != nil { + return err + } + + r.Output.LogInfo("Successfully registered credential for %q cloud provider. Tokens may take up to 30 seconds to refresh.", "azure") + + return nil +} diff --git a/pkg/cli/cmd/credential/register/azure/azure_test.go b/pkg/cli/cmd/credential/register/azure/sp/serviceprincipal_test.go similarity index 67% rename from pkg/cli/cmd/credential/register/azure/azure_test.go rename to pkg/cli/cmd/credential/register/azure/sp/serviceprincipal_test.go index f17c51b023..537dba1cd8 100644 --- a/pkg/cli/cmd/credential/register/azure/azure_test.go +++ b/pkg/cli/cmd/credential/register/azure/sp/serviceprincipal_test.go @@ -14,18 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package azure +package sp import ( "context" "fmt" - "path" "testing" "go.uber.org/mock/gomock" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" - "github.com/radius-project/radius/pkg/cli" "github.com/radius-project/radius/pkg/cli/cmd/credential/common" "github.com/radius-project/radius/pkg/cli/connections" cli_credential "github.com/radius-project/radius/pkg/cli/credential" @@ -36,7 +34,6 @@ import ( ucp "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" "github.com/radius-project/radius/test/radcli" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) func Test_CommandValidation(t *testing.T) { @@ -104,15 +101,6 @@ func Test_Validate(t *testing.T) { ExpectedValid: false, ConfigHolder: framework.ConfigHolder{Config: configWithWorkspace}, }, - { - Name: "Azure command without subscription", - Input: []string{ - "--client-id", "abcd", - "--client-secret", "efgh", - }, - ExpectedValid: false, - ConfigHolder: framework.ConfigHolder{Config: configWithWorkspace}, - }, } radcli.SharedValidateValidation(t, NewCommand, testcases) } @@ -122,44 +110,6 @@ func Test_Run(t *testing.T) { t.Run("Success", func(t *testing.T) { ctrl := gomock.NewController(t) - // We need to isolate the configuration because we're going to make edits - configPath := path.Join(t.TempDir(), "config.yaml") - - yamlData, err := yaml.Marshal(map[string]any{ - "workspaces": cli.WorkspaceSection{ - Default: "a", - Items: map[string]workspaces.Workspace{ - "a": { - Connection: map[string]any{ - "kind": workspaces.KindKubernetes, - "context": "my-context", - }, - Source: workspaces.SourceUserConfig, - - // Will have provider info added - }, - "b": { - Connection: map[string]any{ - "kind": workspaces.KindKubernetes, - "context": "my-context", - }, - Source: workspaces.SourceUserConfig, - }, - "c": { - Connection: map[string]any{ - "kind": workspaces.KindKubernetes, - "context": "my-other-context", - }, - Source: workspaces.SourceUserConfig, - }, - }, - }, - }) - require.NoError(t, err) - - config := radcli.LoadConfig(t, string(yamlData)) - config.SetConfigFile(configPath) - expectedPut := ucp.AzureCredentialResource{ Location: to.Ptr(v1.LocationGlobal), Type: to.Ptr(cli_credential.AzureCredential), @@ -184,10 +134,6 @@ func Test_Run(t *testing.T) { outputSink := &output.MockOutput{} runner := &Runner{ - ConfigHolder: &framework.ConfigHolder{ - Config: config, - ConfigFilePath: configPath, - }, ConnectionFactory: &connections.MockFactory{CredentialManagementClient: client}, Output: outputSink, Workspace: &workspaces.Workspace{ @@ -205,7 +151,7 @@ func Test_Run(t *testing.T) { KubeContext: "my-context", } - err = runner.Run(context.Background()) + err := runner.Run(context.Background()) require.NoError(t, err) expected := []any{ @@ -219,40 +165,6 @@ func Test_Run(t *testing.T) { }, } require.Equal(t, expected, outputSink.Writes) - - expectedConfig := cli.WorkspaceSection{ - Default: "a", - Items: map[string]workspaces.Workspace{ - "a": { - Name: "a", - Connection: map[string]any{ - "kind": workspaces.KindKubernetes, - "context": "my-context", - }, - Source: workspaces.SourceUserConfig, - }, - "b": { - Name: "b", - Connection: map[string]any{ - "kind": workspaces.KindKubernetes, - "context": "my-context", - }, - Source: workspaces.SourceUserConfig, - }, - "c": { - Name: "c", - Connection: map[string]any{ - "kind": workspaces.KindKubernetes, - "context": "my-other-context", - }, - Source: workspaces.SourceUserConfig, - }, - }, - } - - actualConfig, err := cli.ReadWorkspaceSection(config) - require.NoError(t, err) - require.Equal(t, expectedConfig, actualConfig) }) }) } diff --git a/pkg/cli/cmd/credential/register/azure/wi/workloadidentity.go b/pkg/cli/cmd/credential/register/azure/wi/workloadidentity.go new file mode 100644 index 0000000000..fff1a62690 --- /dev/null +++ b/pkg/cli/cmd/credential/register/azure/wi/workloadidentity.go @@ -0,0 +1,156 @@ +/* +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 wi + +import ( + "context" + "fmt" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/credential/common" + "github.com/radius-project/radius/pkg/cli/connections" + cli_credential "github.com/radius-project/radius/pkg/cli/credential" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" + ucp "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the command and runner for the `rad credential create azure wi` command. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "wi", + Short: "Register (Add or update) Azure cloud provider workload identity credential for a Radius installation.", + Long: `Register (Add or update) Azure cloud provider workload identity credential for a Radius installation. + +This command is intended for scripting or advanced use-cases. See 'rad init' for a user-friendly way +to configure these settings. + +Radius will use the provided workload identity credential for all interactions with Azure, including Bicep deployment, +Radius Environments, and Radius portable resources. + +Radius will use the provided subscription and resource group as the default target scope for Bicep deployment. +The provided service principal must have the Contributor or Owner role assigned for the provided resource group +in order to create or manage resources contained in the group. The resource group should be created before +calling 'rad credential register azure wi'. +` + common.LongDescriptionBlurb, + Example: ` +# Register (Add or update) cloud provider credential for Azure with workload identity authentication +rad credential register azure wi --client-id --tenant-id +`, + Args: cobra.ExactArgs(0), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + cmd.Flags().StringVar(&runner.ClientID, "client-id", "", "The client id or app id of an Azure service principal.") + _ = cmd.MarkFlagRequired("client-id") + + cmd.Flags().StringVar(&runner.TenantID, "tenant-id", "", "The tenant id of an Azure service principal.") + _ = cmd.MarkFlagRequired("tenant-id") + + return cmd, runner +} + +// Runner is the runner implementation for the `rad credential register azure wi` command. +type Runner struct { + ConfigHolder *framework.ConfigHolder + ConnectionFactory connections.Factory + Output output.Interface + Format string + Workspace *workspaces.Workspace + + ClientID string + TenantID string + KubeContext string +} + +// NewRunner creates a new instance of the `rad credential register azure wi` runner. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConfigHolder: factory.GetConfigHolder(), + ConnectionFactory: factory.GetConnectionFactory(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad credential register azure wi` command. +// + +// Validate checks for the presence of a workspace, output format, client ID, and tenant ID, and +// sets them in the Runner struct if they are present. If any of these are not present, an error is returned. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + return nil +} + +// Run runs the `rad credential register azure wi` command. +// + +// Run registers a credential for the Azure cloud provider in the Radius installation, updates the server-side +// to add/change credentials. It returns an error if any of the steps fail. +func (r *Runner) Run(ctx context.Context) error { + r.Output.LogInfo("Registering credential for %q cloud provider in Radius installation %q...", "azure", r.Workspace.FmtConnection()) + client, err := r.ConnectionFactory.CreateCredentialManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + credential := ucp.AzureCredentialResource{ + Location: to.Ptr(v1.LocationGlobal), + Type: to.Ptr(cli_credential.AzureCredential), + ID: to.Ptr(fmt.Sprintf(common.AzureCredentialID, "default")), + Properties: &ucp.AzureWorkloadIdentityProperties{ + Storage: &ucp.CredentialStorageProperties{ + Kind: to.Ptr(ucp.CredentialStorageKindInternal), + }, + TenantID: &r.TenantID, + ClientID: &r.ClientID, + Kind: to.Ptr(ucp.AzureCredentialKindWorkloadIdentity), + }, + } + + // Update server-side to add/change credentials + err = client.PutAzure(ctx, credential) + if err != nil { + return err + } + + r.Output.LogInfo("Successfully registered credential for %q cloud provider. Tokens may take up to 30 seconds to refresh.", "azure") + + return nil +} diff --git a/pkg/cli/cmd/credential/register/azure/wi/workloadidentity_test.go b/pkg/cli/cmd/credential/register/azure/wi/workloadidentity_test.go new file mode 100644 index 0000000000..01819fa3f8 --- /dev/null +++ b/pkg/cli/cmd/credential/register/azure/wi/workloadidentity_test.go @@ -0,0 +1,154 @@ +/* +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 wi + +import ( + "context" + "fmt" + "testing" + + "go.uber.org/mock/gomock" + + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/cli/cmd/credential/common" + "github.com/radius-project/radius/pkg/cli/connections" + cli_credential "github.com/radius-project/radius/pkg/cli/credential" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" + ucp "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + configWithWorkspace := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + { + Name: "Valid Azure command", + Input: []string{ + "--client-id", "abcd", + "--tenant-id", "ijkl", + }, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: configWithWorkspace}, + }, + { + Name: "Azure command with fallback workspace", + Input: []string{ + "--client-id", "abcd", + "--tenant-id", "ijkl", + }, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: radcli.LoadEmptyConfig(t)}, + }, + { + Name: "Azure command with too many positional args", + Input: []string{ + "letsgoooooo", + "--client-id", "abcd", + "--tenant-id", "ijkl", + }, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: configWithWorkspace}, + }, + { + Name: "Azure command without client-id", + Input: []string{ + "--tenant-id", "ijkl", + }, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: configWithWorkspace}, + }, + { + Name: "Azure command without tenant-id", + Input: []string{ + "--client-id", "abcd", + }, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: configWithWorkspace}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Create azure provider", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + + expectedPut := ucp.AzureCredentialResource{ + Location: to.Ptr(v1.LocationGlobal), + Type: to.Ptr(cli_credential.AzureCredential), + ID: to.Ptr(fmt.Sprintf(common.AzureCredentialID, "default")), + Properties: &ucp.AzureWorkloadIdentityProperties{ + Storage: &ucp.CredentialStorageProperties{ + Kind: to.Ptr(ucp.CredentialStorageKindInternal), + }, + ClientID: to.Ptr("cool-client-id"), + TenantID: to.Ptr("cool-tenant-id"), + Kind: to.Ptr(ucp.AzureCredentialKindWorkloadIdentity), + }, + } + + client := cli_credential.NewMockCredentialManagementClient(ctrl) + client.EXPECT(). + PutAzure(gomock.Any(), expectedPut). + Return(nil). + Times(1) + + outputSink := &output.MockOutput{} + + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{CredentialManagementClient: client}, + Output: outputSink, + Workspace: &workspaces.Workspace{ + Connection: map[string]any{ + "kind": workspaces.KindKubernetes, + "context": "my-context", + }, + Source: workspaces.SourceUserConfig, + }, + Format: "table", + + ClientID: "cool-client-id", + TenantID: "cool-tenant-id", + KubeContext: "my-context", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "Registering credential for %q cloud provider in Radius installation %q...", + Params: []any{"azure", "Kubernetes (context=my-context)"}, + }, + output.LogOutput{ + Format: "Successfully registered credential for %q cloud provider. Tokens may take up to 30 seconds to refresh.", + Params: []any{"azure"}, + }, + } + require.Equal(t, expected, outputSink.Writes) + }) + }) +} diff --git a/pkg/cli/cmd/credential/register/register.go b/pkg/cli/cmd/credential/register/register.go index 682bbb3ce5..b8b340ffe7 100644 --- a/pkg/cli/cmd/credential/register/register.go +++ b/pkg/cli/cmd/credential/register/register.go @@ -32,17 +32,19 @@ func NewCommand(factory framework.Factory) *cobra.Command { // This command is not runnable, and thus has no runner. cmd := &cobra.Command{ Use: "register", - Short: "Register(Add or update) cloud provider credential for a Radius installation.", + Short: "Register (Add or update) cloud provider credential for a Radius installation.", Long: "Register (Add or update) cloud provider configuration for a Radius installation." + common.LongDescriptionBlurb, Example: ` # Register (Add or update) cloud provider credential for Azure with service principal authentication -rad credential register azure --client-id --client-secret --tenant-id +rad credential register azure sp --client-id --client-secret --tenant-id +# Register (Add or update) cloud provider credential for Azure with workload identity authentication +rad credential register azure wi --client-id --tenant-id # Register (Add or Update) cloud provider credential for AWS with IAM authentication rad credential register aws --access-key-id --secret-access-key `, } - azure, _ := credential_register_azure.NewCommand(factory) + azure := credential_register_azure.NewCommand(factory) cmd.AddCommand(azure) aws, _ := credential_register_aws.NewCommand(factory) diff --git a/pkg/cli/cmd/credential/show/objectformats.go b/pkg/cli/cmd/credential/show/objectformats.go index 9ec4d33769..bf395075da 100644 --- a/pkg/cli/cmd/credential/show/objectformats.go +++ b/pkg/cli/cmd/credential/show/objectformats.go @@ -17,52 +17,78 @@ limitations under the License. package show import ( - "strings" - "github.com/radius-project/radius/pkg/cli/output" ) -// credentialFormat function returns a FormatterOptions struct based on the credentialType parameter, which can -// be either "azure" or "aws". -func credentialFormat(credentialType string) output.FormatterOptions { - if strings.EqualFold(credentialType, "azure") { - return output.FormatterOptions{ - Columns: []output.Column{ - { - Heading: "NAME", - JSONPath: "{ .Name }", - }, - { - Heading: "REGISTERED", - JSONPath: "{ .Enabled }", - }, - { - Heading: "CLIENTID", - JSONPath: "{ .AzureCredentials.ClientID }", - }, - { - Heading: "TENANTID", - JSONPath: "{ .AzureCredentials.TenantID }", - }, +func credentialFormatAzureServicePrincipal() output.FormatterOptions { + return output.FormatterOptions{ + Columns: []output.Column{ + { + Heading: "NAME", + JSONPath: "{ .Name }", + }, + { + Heading: "REGISTERED", + JSONPath: "{ .Enabled }", + }, + { + Heading: "KIND", + JSONPath: "{ .AzureCredentials.Kind }", + }, + { + Heading: "CLIENTID", + JSONPath: "{ .AzureCredentials.ServicePrincipal.ClientID }", + }, + { + Heading: "TENANTID", + JSONPath: "{ .AzureCredentials.ServicePrincipal.TenantID }", + }, + }, + } +} + +func credentialFormatAzureWorkloadIdentity() output.FormatterOptions { + return output.FormatterOptions{ + Columns: []output.Column{ + { + Heading: "NAME", + JSONPath: "{ .Name }", + }, + { + Heading: "REGISTERED", + JSONPath: "{ .Enabled }", + }, + { + Heading: "KIND", + JSONPath: "{ .AzureCredentials.Kind }", + }, + { + Heading: "CLIENTID", + JSONPath: "{ .AzureCredentials.WorkloadIdentity.ClientID }", + }, + { + Heading: "TENANTID", + JSONPath: "{ .AzureCredentials.WorkloadIdentity.TenantID }", + }, + }, + } +} + +func credentialFormatAWS() output.FormatterOptions { + return output.FormatterOptions{ + Columns: []output.Column{ + { + Heading: "NAME", + JSONPath: "{ .Name }", + }, + { + Heading: "REGISTERED", + JSONPath: "{ .Enabled }", }, - } - } else if strings.EqualFold(credentialType, "aws") { - return output.FormatterOptions{ - Columns: []output.Column{ - { - Heading: "NAME", - JSONPath: "{ .Name }", - }, - { - Heading: "REGISTERED", - JSONPath: "{ .Enabled }", - }, - { - Heading: "ACCESSKEYID", - JSONPath: "{ .AWSCredentials.AccessKeyID }", - }, + { + Heading: "ACCESSKEYID", + JSONPath: "{ .AWSCredentials.AccessKeyID }", }, - } + }, } - return output.FormatterOptions{} } diff --git a/pkg/cli/cmd/credential/show/objectformats_test.go b/pkg/cli/cmd/credential/show/objectformats_test.go index 07372a1eba..0a455fe797 100644 --- a/pkg/cli/cmd/credential/show/objectformats_test.go +++ b/pkg/cli/cmd/credential/show/objectformats_test.go @@ -26,23 +26,53 @@ import ( "github.com/stretchr/testify/require" ) -func Test_credentialFormat_Azure(t *testing.T) { +func Test_credentialFormatAzureServicePrincipal(t *testing.T) { obj := credential.ProviderCredentialConfiguration{ CloudProviderStatus: credential.CloudProviderStatus{ Name: "test", Enabled: true, }, AzureCredentials: &credential.AzureCredentialProperties{ - ClientID: to.Ptr("test-client-id"), - TenantID: to.Ptr("test-tenant-id"), + Kind: to.Ptr("ServicePrincipal"), + ServicePrincipal: &credential.AzureServicePrincipalCredentialProperties{ + ClientID: to.Ptr("test-client-id"), + TenantID: to.Ptr("test-tenant-id"), + }, }, } buffer := &bytes.Buffer{} - err := output.Write(output.FormatTable, obj, buffer, credentialFormat("azure")) + credentialFormatOutput := credentialFormatAzureServicePrincipal() + + err := output.Write(output.FormatTable, obj, buffer, credentialFormatOutput) + require.NoError(t, err) + + expected := "NAME REGISTERED KIND CLIENTID TENANTID\ntest true ServicePrincipal test-client-id test-tenant-id\n" + require.Equal(t, expected, buffer.String()) +} + +func Test_credentialFormat_Azure_WorkloadIdentity(t *testing.T) { + obj := credential.ProviderCredentialConfiguration{ + CloudProviderStatus: credential.CloudProviderStatus{ + Name: "test", + Enabled: true, + }, + AzureCredentials: &credential.AzureCredentialProperties{ + Kind: to.Ptr("WorkloadIdentity"), + WorkloadIdentity: &credential.AzureWorkloadIdentityCredentialProperties{ + ClientID: to.Ptr("test-client-id"), + TenantID: to.Ptr("test-tenant-id"), + }, + }, + } + + buffer := &bytes.Buffer{} + credentialFormatOutput := credentialFormatAzureWorkloadIdentity() + + err := output.Write(output.FormatTable, obj, buffer, credentialFormatOutput) require.NoError(t, err) - expected := "NAME REGISTERED CLIENTID TENANTID\ntest true test-client-id test-tenant-id\n" + expected := "NAME REGISTERED KIND CLIENTID TENANTID\ntest true WorkloadIdentity test-client-id test-tenant-id\n" require.Equal(t, expected, buffer.String()) } @@ -58,7 +88,9 @@ func Test_credentialFormat_AWS(t *testing.T) { } buffer := &bytes.Buffer{} - err := output.Write(output.FormatTable, obj, buffer, credentialFormat("aws")) + credentialFormatOutput := credentialFormatAWS() + + err := output.Write(output.FormatTable, obj, buffer, credentialFormatOutput) require.NoError(t, err) expected := "NAME REGISTERED ACCESSKEYID\ntest true test-access-key-id\n" diff --git a/pkg/cli/cmd/credential/show/show.go b/pkg/cli/cmd/credential/show/show.go index a65604896d..0bd0d6e33b 100644 --- a/pkg/cli/cmd/credential/show/show.go +++ b/pkg/cli/cmd/credential/show/show.go @@ -18,6 +18,7 @@ package show import ( "context" + "fmt" "github.com/radius-project/radius/pkg/cli" "github.com/radius-project/radius/pkg/cli/clierrors" @@ -27,6 +28,7 @@ import ( "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/ucp/datamodel" "github.com/spf13/cobra" ) @@ -125,7 +127,25 @@ func (r *Runner) Run(ctx context.Context) error { if !providers.Enabled { return clierrors.Message("The credentials for cloud provider %q could not be found.", r.Kind) } - err = r.Output.WriteFormatted(r.Format, providers, credentialFormat(r.Kind)) + + var output output.FormatterOptions + switch r.Kind { + case "azure": + switch *providers.AzureCredentials.Kind { + case datamodel.AzureServicePrincipalCredentialKind: + output = credentialFormatAzureServicePrincipal() + case datamodel.AzureWorkloadIdentityCredentialKind: + output = credentialFormatAzureWorkloadIdentity() + default: + return fmt.Errorf("unknown Azure credential kind, expected ServicePrincipal or WorkloadIdentity (got %s)", *providers.AzureCredentials.Kind) + } + case "aws": + output = credentialFormatAWS() + default: + return fmt.Errorf("unknown credential type: %s", r.Kind) + } + + err = r.Output.WriteFormatted(r.Format, providers, output) if err != nil { return err } diff --git a/pkg/cli/cmd/credential/show/show_test.go b/pkg/cli/cmd/credential/show/show_test.go index 50ca9247c7..9b975b9749 100644 --- a/pkg/cli/cmd/credential/show/show_test.go +++ b/pkg/cli/cmd/credential/show/show_test.go @@ -26,6 +26,7 @@ import ( "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/radcli" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -111,6 +112,9 @@ func Test_Run(t *testing.T) { Name: "azure", Enabled: true, }, + AzureCredentials: &cli_credential.AzureCredentialProperties{ + Kind: to.Ptr("ServicePrincipal"), + }, } client := cli_credential.NewMockCredentialManagementClient(ctrl) @@ -132,6 +136,8 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) + credentialFormatOutput := credentialFormatAzureServicePrincipal() + expected := []any{ output.LogOutput{ Format: "Showing credential for cloud provider %q for Radius installation %q...", @@ -140,7 +146,7 @@ func Test_Run(t *testing.T) { output.FormattedOutput{ Format: "table", Obj: provider, - Options: credentialFormat(runner.Kind), + Options: credentialFormatOutput, }, } require.Equal(t, expected, outputSink.Writes) @@ -199,6 +205,8 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) + credentialFormatOutput := credentialFormatAWS() + expected := []any{ output.LogOutput{ Format: "Showing credential for cloud provider %q for Radius installation %q...", @@ -207,7 +215,7 @@ func Test_Run(t *testing.T) { output.FormattedOutput{ Format: "table", Obj: provider, - Options: credentialFormat(runner.Kind), + Options: credentialFormatOutput, }, } require.Equal(t, expected, outputSink.Writes) diff --git a/pkg/cli/cmd/radinit/azure.go b/pkg/cli/cmd/radinit/azure.go index 045a6926d3..d28011f21f 100644 --- a/pkg/cli/cmd/radinit/azure.go +++ b/pkg/cli/cmd/radinit/azure.go @@ -37,16 +37,24 @@ const ( enterAzureResourceGroupNamePlaceholder = "Enter resource group name" selectAzureResourceGroupLocationPrompt = "Select a location for the resource group:" selectAzureResourceGroupPrompt = "Select a resource group:" + selectAzureCredentialKindPrompt = "Select a credential kind for the Azure credential:" enterAzureServicePrincipalAppIDPrompt = "Enter the `appId` of the service principal used to create Azure resources" enterAzureServicePrincipalAppIDPlaceholder = "Enter appId..." enterAzureServicePrincipalPasswordPrompt = "Enter the `password` of the service principal used to create Azure resources" enterAzureServicePrincipalPasswordPlaceholder = "Enter password..." enterAzureServicePrincipalTenantIDPrompt = "Enter the `tenantId` of the service principal used to create Azure resources" enterAzureServicePrincipalTenantIDPlaceholder = "Enter tenantId..." + enterAzureWorkloadIdentityAppIDPrompt = "Enter the `appId` of the Entra ID Application" + enterAzureWorkloadIdentityAppIDPlaceholder = "Enter appId..." + enterAzureWorkloadIdentityTenantIDPrompt = "Enter the `tenantId` of the Entra ID Application" + enterAzureWorkloadIdentityTenantIDPlaceholder = "Enter tenantId..." + azureWorkloadIdentityCreateInstructionsFmt = "\nA workload identity federated credential is required to create Azure resources. Please follow the guidance at aka.ms/rad-workload-identity to set up workload identity for Radius.\n\n" azureServicePrincipalCreateInstructionsFmt = "\nAn Azure service principal with a corresponding role assignment on your resource group is required to create Azure resources.\n\nFor example, you can create one using the following command:\n\033[36maz ad sp create-for-rbac --role Owner --scope /subscriptions/%s/resourceGroups/%s\033[0m\n\nFor more information refer to https://docs.microsoft.com/cli/azure/ad/sp?view=azure-cli-latest#az-ad-sp-create-for-rbac and https://aka.ms/azadsp-more\n\n" + azureServicePrincipalCredentialKind = "Service Principal" + azureWorkloadIdenityCredentialKind = "Workload Identity" ) -func (r *Runner) enterAzureCloudProvider(ctx context.Context) (*azure.Provider, error) { +func (r *Runner) enterAzureCloudProvider(ctx context.Context, options *initOptions) (*azure.Provider, error) { subscription, err := r.selectAzureSubscription(ctx) if err != nil { return nil, err @@ -57,38 +65,80 @@ func (r *Runner) enterAzureCloudProvider(ctx context.Context) (*azure.Provider, return nil, err } - r.Output.LogInfo(azureServicePrincipalCreateInstructionsFmt, subscription.ID, resourceGroup) - - clientID, err := r.Prompter.GetTextInput(enterAzureServicePrincipalAppIDPrompt, prompt.TextInputOptions{ - Placeholder: enterAzureServicePrincipalAppIDPlaceholder, - Validate: prompt.ValidateUUIDv4, - }) + credentialKind, err := r.selectAzureCredentialKind() if err != nil { return nil, err } - clientSecret, err := r.Prompter.GetTextInput(enterAzureServicePrincipalPasswordPrompt, prompt.TextInputOptions{Placeholder: enterAzureServicePrincipalPasswordPlaceholder, EchoMode: textinput.EchoPassword}) - if err != nil { - return nil, err - } + switch credentialKind { + case azureServicePrincipalCredentialKind: + r.Output.LogInfo(azureServicePrincipalCreateInstructionsFmt, subscription.ID, resourceGroup) - tenantID, err := r.Prompter.GetTextInput(enterAzureServicePrincipalTenantIDPrompt, prompt.TextInputOptions{ - Placeholder: enterAzureServicePrincipalTenantIDPlaceholder, - Validate: prompt.ValidateUUIDv4, - }) - if err != nil { - return nil, err - } + clientID, err := r.Prompter.GetTextInput(enterAzureServicePrincipalAppIDPrompt, prompt.TextInputOptions{ + Placeholder: enterAzureServicePrincipalAppIDPlaceholder, + Validate: prompt.ValidateUUIDv4, + }) + if err != nil { + return nil, err + } - return &azure.Provider{ - SubscriptionID: subscription.ID, - ResourceGroup: resourceGroup, - ServicePrincipal: &azure.ServicePrincipal{ - ClientID: clientID, - ClientSecret: clientSecret, - TenantID: tenantID, - }, - }, nil + clientSecret, err := r.Prompter.GetTextInput(enterAzureServicePrincipalPasswordPrompt, prompt.TextInputOptions{Placeholder: enterAzureServicePrincipalPasswordPlaceholder, EchoMode: textinput.EchoPassword}) + if err != nil { + return nil, err + } + + tenantID, err := r.Prompter.GetTextInput(enterAzureServicePrincipalTenantIDPrompt, prompt.TextInputOptions{ + Placeholder: enterAzureServicePrincipalTenantIDPlaceholder, + Validate: prompt.ValidateUUIDv4, + }) + if err != nil { + return nil, err + } + + return &azure.Provider{ + SubscriptionID: subscription.ID, + ResourceGroup: resourceGroup, + CredentialKind: azure.AzureCredentialKindServicePrincipal, + ServicePrincipal: &azure.ServicePrincipalCredential{ + ClientID: clientID, + ClientSecret: clientSecret, + TenantID: tenantID, + }, + }, nil + case azureWorkloadIdenityCredentialKind: + r.Output.LogInfo(azureWorkloadIdentityCreateInstructionsFmt) + + clientID, err := r.Prompter.GetTextInput(enterAzureWorkloadIdentityAppIDPrompt, prompt.TextInputOptions{ + Placeholder: enterAzureWorkloadIdentityAppIDPlaceholder, + Validate: prompt.ValidateUUIDv4, + }) + if err != nil { + return nil, err + } + + tenantID, err := r.Prompter.GetTextInput(enterAzureWorkloadIdentityTenantIDPrompt, prompt.TextInputOptions{ + Placeholder: enterAzureWorkloadIdentityTenantIDPlaceholder, + Validate: prompt.ValidateUUIDv4, + }) + if err != nil { + return nil, err + } + + // Set the value for the Helm chart + options.SetValues = append(options.SetValues, "global.azureWorkloadIdentity.enabled=true") + + return &azure.Provider{ + SubscriptionID: subscription.ID, + ResourceGroup: resourceGroup, + CredentialKind: azure.AzureCredentialKindWorkloadIdentity, + WorkloadIdentity: &azure.WorkloadIdentityCredential{ + ClientID: clientID, + TenantID: tenantID, + }, + }, nil + default: + return nil, clierrors.Message("Invalid Azure credential kind: %s", credentialKind) + } } func (r *Runner) selectAzureSubscription(ctx context.Context) (*azure.Subscription, error) { @@ -119,6 +169,15 @@ func (r *Runner) selectAzureSubscription(ctx context.Context) (*azure.Subscripti return &subscription, nil } +func (r *Runner) selectAzureCredentialKind() (string, error) { + credentialKinds, err := r.buildAzureCredentialKind() + if err != nil { + return "", err + } + + return r.Prompter.GetListInput(credentialKinds, selectAzureCredentialKindPrompt) +} + // buildSubscriptionListAndMap builds a list of subscription names, as well as a map of name => subcription. We need the list // to build the prompt, and the map to look up the subscription object by name after the user makes a selection. func (r *Runner) buildAzureSubscriptionListAndMap(subscriptions *azure.SubscriptionResult) ([]string, map[string]azure.Subscription) { @@ -244,3 +303,10 @@ func (r *Runner) buildAzureResourceGroupLocationListAndMap(locations []armsubscr return names, locationMap } + +func (r *Runner) buildAzureCredentialKind() ([]string, error) { + return []string{ + azureServicePrincipalCredentialKind, + azureWorkloadIdenityCredentialKind, + }, nil +} diff --git a/pkg/cli/cmd/radinit/azure_test.go b/pkg/cli/cmd/radinit/azure_test.go index a5f208e600..de7b88bfb9 100644 --- a/pkg/cli/cmd/radinit/azure_test.go +++ b/pkg/cli/cmd/radinit/azure_test.go @@ -31,7 +31,7 @@ import ( "go.uber.org/mock/gomock" ) -func Test_enterAzureCloudProvider(t *testing.T) { +func Test_enterAzureCloudProvider_ServicePrincipal(t *testing.T) { ctrl := gomock.NewController(t) prompter := prompt.NewMockInterface(ctrl) client := azure.NewMockClient(ctrl) @@ -47,27 +47,29 @@ func Test_enterAzureCloudProvider(t *testing.T) { Name: to.Ptr("test-resource-group"), } - // selectAzureSubscription setAzureSubscriptions(client, &azure.SubscriptionResult{Default: &subscription, Subscriptions: []azure.Subscription{subscription}}) setAzureSubscriptionConfirmPrompt(prompter, subscription.Name, prompt.ConfirmYes) - // selectAzureResourceGroup setAzureResourceGroupCreatePrompt(prompter, prompt.ConfirmNo) setAzureResourceGroups(client, subscription.ID, []armresources.ResourceGroup{resourceGroup}) setAzureResourceGroupPrompt(prompter, []string{*resourceGroup.Name}, *resourceGroup.Name) - // service principal + setAzureCredentialKindPrompt(prompter, "Service Principal") + setAzureServicePrincipalAppIDPrompt(prompter, "service-principal-app-id") setAzureServicePrincipalPasswordPrompt(prompter, "service-principal-password") setAzureServicePrincipalTenantIDPrompt(prompter, "service-principal-tenant-id") - provider, err := runner.enterAzureCloudProvider(context.Background()) + options := &initOptions{} + + provider, err := runner.enterAzureCloudProvider(context.Background(), options) require.NoError(t, err) expected := &azure.Provider{ SubscriptionID: subscription.ID, ResourceGroup: *resourceGroup.Name, - ServicePrincipal: &azure.ServicePrincipal{ + CredentialKind: "ServicePrincipal", + ServicePrincipal: &azure.ServicePrincipalCredential{ ClientID: "service-principal-app-id", ClientSecret: "service-principal-password", TenantID: "service-principal-tenant-id", @@ -80,6 +82,63 @@ func Test_enterAzureCloudProvider(t *testing.T) { Params: []any{subscription.ID, *resourceGroup.Name}, }} require.Equal(t, expectedOutput, outputSink.Writes) + + expectedOptions := &initOptions{} + require.Equal(t, expectedOptions, options) +} + +func Test_enterAzureCloudProvider_WorkloadIdentity(t *testing.T) { + ctrl := gomock.NewController(t) + prompter := prompt.NewMockInterface(ctrl) + client := azure.NewMockClient(ctrl) + outputSink := output.MockOutput{} + runner := Runner{Prompter: prompter, azureClient: client, Output: &outputSink} + + subscription := azure.Subscription{ + Name: "test-subscription", + ID: "test-subscription-id", + } + + resourceGroup := armresources.ResourceGroup{ + Name: to.Ptr("test-resource-group"), + } + + setAzureSubscriptions(client, &azure.SubscriptionResult{Default: &subscription, Subscriptions: []azure.Subscription{subscription}}) + setAzureSubscriptionConfirmPrompt(prompter, subscription.Name, prompt.ConfirmYes) + + setAzureResourceGroupCreatePrompt(prompter, prompt.ConfirmNo) + setAzureResourceGroups(client, subscription.ID, []armresources.ResourceGroup{resourceGroup}) + setAzureResourceGroupPrompt(prompter, []string{*resourceGroup.Name}, *resourceGroup.Name) + + setAzureCredentialKindPrompt(prompter, "Workload Identity") + + setAzureWorkloadIdentityAppIDPrompt(prompter, "service-principal-app-id") + setAzureWorkloadIdentityTenantIDPrompt(prompter, "service-principal-tenant-id") + + options := &initOptions{} + + provider, err := runner.enterAzureCloudProvider(context.Background(), options) + require.NoError(t, err) + + expected := &azure.Provider{ + SubscriptionID: subscription.ID, + ResourceGroup: *resourceGroup.Name, + CredentialKind: "WorkloadIdentity", + WorkloadIdentity: &azure.WorkloadIdentityCredential{ + ClientID: "service-principal-app-id", + TenantID: "service-principal-tenant-id", + }, + } + require.Equal(t, expected, provider) + + expectedOutput := []any{output.LogOutput{ + Format: azureWorkloadIdentityCreateInstructionsFmt, + }} + require.Equal(t, expectedOutput, outputSink.Writes) + + expectedOptions := &initOptions{} + expectedOptions.SetValues = []string{"global.azureWorkloadIdentity.enabled=true"} + require.Equal(t, expectedOptions, options) } func Test_selectAzureSubscription(t *testing.T) { diff --git a/pkg/cli/cmd/radinit/cloud.go b/pkg/cli/cmd/radinit/cloud.go index 6d92f303c1..e492d439cd 100644 --- a/pkg/cli/cmd/radinit/cloud.go +++ b/pkg/cli/cmd/radinit/cloud.go @@ -57,7 +57,7 @@ func (r *Runner) enterCloudProviderOptions(ctx context.Context, options *initOpt switch cloudProvider { case azure.ProviderDisplayName: - provider, err := r.enterAzureCloudProvider(ctx) + provider, err := r.enterAzureCloudProvider(ctx, options) if err != nil { return err } diff --git a/pkg/cli/cmd/radinit/cloud_test.go b/pkg/cli/cmd/radinit/cloud_test.go index 7587608a26..f9949673b5 100644 --- a/pkg/cli/cmd/radinit/cloud_test.go +++ b/pkg/cli/cmd/radinit/cloud_test.go @@ -29,16 +29,27 @@ import ( ) func Test_enterCloudProviderOptions(t *testing.T) { - azureProvider := azure.Provider{ + azureProviderServicePrincipal := azure.Provider{ SubscriptionID: "test-subscription-id", ResourceGroup: "test-resource-group", - ServicePrincipal: &azure.ServicePrincipal{ + CredentialKind: azure.AzureCredentialKindServicePrincipal, + ServicePrincipal: &azure.ServicePrincipalCredential{ ClientID: "test-client-id", ClientSecret: "test-client-secret", TenantID: "test-tenant-id", }, } + azureProviderWorkloadIdentity := azure.Provider{ + SubscriptionID: "test-subscription-id", + ResourceGroup: "test-resource-group", + CredentialKind: azure.AzureCredentialKindWorkloadIdentity, + WorkloadIdentity: &azure.WorkloadIdentityCredential{ + ClientID: "test-client-id", + TenantID: "test-tenant-id", + }, + } + awsProvider := aws.Provider{ Region: "test-region", AccessKeyID: "test-access-key-id", @@ -130,7 +141,7 @@ func Test_enterCloudProviderOptions(t *testing.T) { require.Equal(t, expectedWrites, outputSink.Writes) }) - t.Run("--full - azure provider", func(t *testing.T) { + t.Run("--full - azure provider - service principal", func(t *testing.T) { ctrl := gomock.NewController(t) prompter := prompt.NewMockInterface(ctrl) awsClient := aws.NewMockClient(ctrl) @@ -140,19 +151,46 @@ func Test_enterCloudProviderOptions(t *testing.T) { initAddCloudProviderPromptYes(prompter) initSelectCloudProvider(prompter, azure.ProviderDisplayName) - setAzureCloudProvider(prompter, azureClient, azureProvider) + setAzureCloudProviderServicePrincipal(prompter, azureClient, azureProviderServicePrincipal) initAddCloudProviderPromptNo(prompter) options := initOptions{Environment: environmentOptions{Create: true}} err := runner.enterCloudProviderOptions(context.Background(), &options) require.NoError(t, err) require.Nil(t, options.CloudProviders.AWS) - require.Equal(t, azureProvider, *options.CloudProviders.Azure) + require.Equal(t, azureProviderServicePrincipal, *options.CloudProviders.Azure) expectedWrites := []any{ output.LogOutput{ Format: azureServicePrincipalCreateInstructionsFmt, - Params: []any{azureProvider.SubscriptionID, azureProvider.ResourceGroup}, + Params: []any{azureProviderServicePrincipal.SubscriptionID, azureProviderServicePrincipal.ResourceGroup}, + }, + } + require.Equal(t, expectedWrites, outputSink.Writes) + }) + + t.Run("--full - azure provider - workload identity", func(t *testing.T) { + ctrl := gomock.NewController(t) + prompter := prompt.NewMockInterface(ctrl) + awsClient := aws.NewMockClient(ctrl) + azureClient := azure.NewMockClient(ctrl) + outputSink := output.MockOutput{} + runner := Runner{Prompter: prompter, awsClient: awsClient, azureClient: azureClient, Output: &outputSink, Full: true} + + initAddCloudProviderPromptYes(prompter) + initSelectCloudProvider(prompter, azure.ProviderDisplayName) + setAzureCloudProviderWorkloadIdentity(prompter, azureClient, azureProviderWorkloadIdentity) + initAddCloudProviderPromptNo(prompter) + + options := initOptions{Environment: environmentOptions{Create: true}} + err := runner.enterCloudProviderOptions(context.Background(), &options) + require.NoError(t, err) + require.Nil(t, options.CloudProviders.AWS) + require.Equal(t, azureProviderWorkloadIdentity, *options.CloudProviders.Azure) + + expectedWrites := []any{ + output.LogOutput{ + Format: azureWorkloadIdentityCreateInstructionsFmt, }, } require.Equal(t, expectedWrites, outputSink.Writes) @@ -172,7 +210,7 @@ func Test_enterCloudProviderOptions(t *testing.T) { initAddCloudProviderPromptYes(prompter) initSelectCloudProvider(prompter, azure.ProviderDisplayName) - setAzureCloudProvider(prompter, azureClient, azureProvider) + setAzureCloudProviderServicePrincipal(prompter, azureClient, azureProviderServicePrincipal) initAddCloudProviderPromptNo(prompter) @@ -180,7 +218,7 @@ func Test_enterCloudProviderOptions(t *testing.T) { err := runner.enterCloudProviderOptions(context.Background(), &options) require.NoError(t, err) require.Equal(t, awsProvider, *options.CloudProviders.AWS) - require.Equal(t, azureProvider, *options.CloudProviders.Azure) + require.Equal(t, azureProviderServicePrincipal, *options.CloudProviders.Azure) expectedWrites := []any{ output.LogOutput{ @@ -188,7 +226,7 @@ func Test_enterCloudProviderOptions(t *testing.T) { }, output.LogOutput{ Format: azureServicePrincipalCreateInstructionsFmt, - Params: []any{azureProvider.SubscriptionID, azureProvider.ResourceGroup}, + Params: []any{azureProviderServicePrincipal.SubscriptionID, azureProviderServicePrincipal.ResourceGroup}, }, } require.Equal(t, expectedWrites, outputSink.Writes) diff --git a/pkg/cli/cmd/radinit/display.go b/pkg/cli/cmd/radinit/display.go index d3759a550b..ead182de57 100644 --- a/pkg/cli/cmd/radinit/display.go +++ b/pkg/cli/cmd/radinit/display.go @@ -26,6 +26,7 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/radius-project/radius/pkg/cli/azure" "github.com/radius-project/radius/pkg/cli/prompt" ) @@ -36,7 +37,7 @@ const ( summaryKubernetesHeadingIcon = "🔧 " summaryKubernetesInstallHeadingFmt = "Install Radius %s\n" + summaryIndent + "Kubernetes cluster: %s\n" + summaryIndent + "Kubernetes namespace: %s\n" summaryKubernetesInstallAWSCloudProviderFmt = summaryIndent + "AWS IAM access key id: %s\n" - summaryKubernetesInstallAzureCloudProviderFmt = summaryIndent + "Azure service principal: %s\n" + summaryKubernetesInstallAzureCloudProviderFmt = summaryIndent + "Azure credential: %s\n" summaryKubernetesExistingHeadingFmt = "Use existing Radius %s install on %s\n" summaryEnvironmentHeadingIcon = "🌏 " summaryEnvironmentCreateHeadingFmt = "Create new environment %s\n" + summaryIndent + "Kubernetes namespace: %s\n" @@ -207,7 +208,13 @@ func (m *summaryModel) View() string { message.WriteString(fmt.Sprintf(summaryKubernetesInstallAWSCloudProviderFmt, highlight(options.CloudProviders.AWS.AccessKeyID))) } if options.CloudProviders.Azure != nil { - message.WriteString(fmt.Sprintf(summaryKubernetesInstallAzureCloudProviderFmt, highlight(options.CloudProviders.Azure.ServicePrincipal.ClientID))) + message.WriteString(fmt.Sprintf(summaryKubernetesInstallAzureCloudProviderFmt, highlight(string(options.CloudProviders.Azure.CredentialKind)))) + switch options.CloudProviders.Azure.CredentialKind { + case azure.AzureCredentialKindServicePrincipal: + message.WriteString(fmt.Sprintf(summaryIndent+"Client ID: %s\n", highlight(options.CloudProviders.Azure.ServicePrincipal.ClientID))) + case azure.AzureCredentialKindWorkloadIdentity: + message.WriteString(fmt.Sprintf(summaryIndent+"Client ID: %s\n", highlight(options.CloudProviders.Azure.WorkloadIdentity.ClientID))) + } } } else { message.WriteString(fmt.Sprintf(summaryKubernetesExistingHeadingFmt, highlight(options.Cluster.Version), highlight(options.Cluster.Context))) @@ -333,7 +340,13 @@ func (m *progressModel) View() string { message.WriteString(fmt.Sprintf(summaryKubernetesInstallAWSCloudProviderFmt, highlight(options.CloudProviders.AWS.AccessKeyID))) } if options.CloudProviders.Azure != nil { - message.WriteString(fmt.Sprintf(summaryKubernetesInstallAzureCloudProviderFmt, highlight(options.CloudProviders.Azure.ServicePrincipal.ClientID))) + message.WriteString(fmt.Sprintf(summaryKubernetesInstallAzureCloudProviderFmt, highlight(string(options.CloudProviders.Azure.CredentialKind)))) + switch options.CloudProviders.Azure.CredentialKind { + case azure.AzureCredentialKindServicePrincipal: + message.WriteString(fmt.Sprintf(summaryIndent+"Client ID: %s\n", highlight(options.CloudProviders.Azure.ServicePrincipal.ClientID))) + case azure.AzureCredentialKindWorkloadIdentity: + message.WriteString(fmt.Sprintf(summaryIndent+"Client ID: %s\n", highlight(options.CloudProviders.Azure.WorkloadIdentity.ClientID))) + } } } else { message.WriteString(fmt.Sprintf(summaryKubernetesExistingHeadingFmt, highlight(options.Cluster.Version), highlight(options.Cluster.Context))) diff --git a/pkg/cli/cmd/radinit/environment.go b/pkg/cli/cmd/radinit/environment.go index e2144306ad..a4542c8cbf 100644 --- a/pkg/cli/cmd/radinit/environment.go +++ b/pkg/cli/cmd/radinit/environment.go @@ -96,8 +96,12 @@ func (r *Runner) CreateEnvironment(ctx context.Context) error { } if r.Options.CloudProviders.Azure != nil { - credential := r.getAzureCredential() - err := credentialClient.PutAzure(ctx, credential) + credential, err := r.getAzureCredential() + if err != nil { + return clierrors.MessageWithCause(err, "Failed to configure Azure credentials.") + } + + err = credentialClient.PutAzure(ctx, credential) if err != nil { return clierrors.MessageWithCause(err, "Failed to configure Azure credentials.") } diff --git a/pkg/cli/cmd/radinit/init.go b/pkg/cli/cmd/radinit/init.go index d22851c328..84133d9619 100644 --- a/pkg/cli/cmd/radinit/init.go +++ b/pkg/cli/cmd/radinit/init.go @@ -18,6 +18,7 @@ package radinit import ( "context" + "fmt" "os" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" @@ -211,8 +212,14 @@ func (r *Runner) Run(ctx context.Context) error { }() if r.Options.Cluster.Install { + cliOptions := helm.CLIClusterOptions{ + Radius: helm.RadiusOptions{ + SetArgs: r.Options.SetValues, + }, + } + // Install radius control plane - err := installRadius(ctx, r) + err := installRadius(ctx, r, cliOptions) if err != nil { return clierrors.MessageWithCause(err, "Failed to install Radius.") } @@ -277,18 +284,35 @@ func (r *Runner) Run(ctx context.Context) error { return nil } -func (r *Runner) getAzureCredential() ucp.AzureCredentialResource { - return ucp.AzureCredentialResource{ - Location: to.Ptr(v1.LocationGlobal), - Type: to.Ptr(cli_credential.AzureCredential), - Properties: &ucp.AzureServicePrincipalProperties{ - Storage: &ucp.CredentialStorageProperties{ - Kind: to.Ptr(ucp.CredentialStorageKindInternal), +func (r *Runner) getAzureCredential() (ucp.AzureCredentialResource, error) { + switch r.Options.CloudProviders.Azure.CredentialKind { + case azure.AzureCredentialKindServicePrincipal: + return ucp.AzureCredentialResource{ + Location: to.Ptr(v1.LocationGlobal), + Type: to.Ptr(cli_credential.AzureCredential), + Properties: &ucp.AzureServicePrincipalProperties{ + Storage: &ucp.CredentialStorageProperties{ + Kind: to.Ptr(ucp.CredentialStorageKindInternal), + }, + TenantID: &r.Options.CloudProviders.Azure.ServicePrincipal.TenantID, + ClientID: &r.Options.CloudProviders.Azure.ServicePrincipal.ClientID, + ClientSecret: &r.Options.CloudProviders.Azure.ServicePrincipal.ClientSecret, }, - TenantID: &r.Options.CloudProviders.Azure.ServicePrincipal.TenantID, - ClientID: &r.Options.CloudProviders.Azure.ServicePrincipal.ClientID, - ClientSecret: &r.Options.CloudProviders.Azure.ServicePrincipal.ClientSecret, - }, + }, nil + case azure.AzureCredentialKindWorkloadIdentity: + return ucp.AzureCredentialResource{ + Location: to.Ptr(v1.LocationGlobal), + Type: to.Ptr(cli_credential.AzureCredential), + Properties: &ucp.AzureWorkloadIdentityProperties{ + Storage: &ucp.CredentialStorageProperties{ + Kind: to.Ptr(ucp.CredentialStorageKindInternal), + }, + TenantID: &r.Options.CloudProviders.Azure.WorkloadIdentity.TenantID, + ClientID: &r.Options.CloudProviders.Azure.WorkloadIdentity.ClientID, + }, + }, nil + default: + return ucp.AzureCredentialResource{}, fmt.Errorf("unsupported Azure credential kind: %s", r.Options.CloudProviders.Azure.CredentialKind) } } @@ -306,11 +330,7 @@ func (r *Runner) getAWSCredential() ucp.AwsCredentialResource { } } -func installRadius(ctx context.Context, r *Runner) error { - cliOptions := helm.CLIClusterOptions{ - Radius: helm.RadiusOptions{}, - } - +func installRadius(ctx context.Context, r *Runner, cliOptions helm.CLIClusterOptions) error { clusterOptions := helm.PopulateDefaultClusterOptions(cliOptions) // Ignore existing radius installation because we already asked the user whether to re-install or not diff --git a/pkg/cli/cmd/radinit/init_test.go b/pkg/cli/cmd/radinit/init_test.go index d37a8dedbc..30b4c51071 100644 --- a/pkg/cli/cmd/radinit/init_test.go +++ b/pkg/cli/cmd/radinit/init_test.go @@ -59,16 +59,27 @@ func Test_CommandValidation(t *testing.T) { func Test_Validate(t *testing.T) { config := radcli.LoadConfigWithWorkspace(t) - azureProvider := azure.Provider{ + azureProviderServicePrincipal := azure.Provider{ SubscriptionID: "test-subscription-id", ResourceGroup: "test-resource-group", - ServicePrincipal: &azure.ServicePrincipal{ + CredentialKind: "ServicePrincipal", + ServicePrincipal: &azure.ServicePrincipalCredential{ ClientID: "test-client-id", ClientSecret: "test-client-secret", TenantID: "test-tenant-id", }, } + azureProviderWorkloadIdentity := azure.Provider{ + SubscriptionID: "test-subscription-id", + ResourceGroup: "test-resource-group", + CredentialKind: "WorkloadIdentity", + WorkloadIdentity: &azure.WorkloadIdentityCredential{ + ClientID: "test-client-id", + TenantID: "test-tenant-id", + }, + } + awsProvider := aws.Provider{ Region: "test-region", AccessKeyID: "test-access-key-id", @@ -244,7 +255,7 @@ func Test_Validate(t *testing.T) { }, }, { - Name: "Init --full Command With Azure Cloud Provider", + Name: "Init --full Command With Azure Cloud Provider - Service Principal", Input: []string{"--full"}, ExpectedValid: true, ConfigHolder: framework.ConfigHolder{ @@ -267,7 +278,42 @@ func Test_Validate(t *testing.T) { // Add azure provider initAddCloudProviderPromptYes(mocks.Prompter) initSelectCloudProvider(mocks.Prompter, azure.ProviderDisplayName) - setAzureCloudProvider(mocks.Prompter, mocks.AzureClient, azureProvider) + setAzureCloudProviderServicePrincipal(mocks.Prompter, mocks.AzureClient, azureProviderServicePrincipal) + + // Don't add any other cloud providers + initAddCloudProviderPromptNo(mocks.Prompter) + + // No application + setScaffoldApplicationPromptNo(mocks.Prompter) + + setConfirmOption(mocks.Prompter, resultConfimed) + }, + }, + { + Name: "Init --full Command With Azure Cloud Provider - Workload Identity", + Input: []string{"--full"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: config, + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + // Radius is already installed + initGetKubeContextSuccess(mocks.Kubernetes) + initKubeContextWithKind(mocks.Prompter) + initHelmMockRadiusInstalled(mocks.Helm) + + // No existing environment, users will be prompted to create a new one + setExistingEnvironments(mocks.ApplicationManagementClient, []corerp.EnvironmentResource{}) + + // Choose default name and namespace + initEnvNamePrompt(mocks.Prompter, "default") + initNamespacePrompt(mocks.Prompter, "default") + + // Add azure provider + initAddCloudProviderPromptYes(mocks.Prompter) + initSelectCloudProvider(mocks.Prompter, azure.ProviderDisplayName) + setAzureCloudProviderWorkloadIdentity(mocks.Prompter, mocks.AzureClient, azureProviderWorkloadIdentity) // Don't add any other cloud providers initAddCloudProviderPromptNo(mocks.Prompter) @@ -609,12 +655,13 @@ func Test_Run_InstallAndCreateEnvironment(t *testing.T) { expectedOutput: []any{}, }, { - name: "`rad init --full` with Azure Provider", + name: "`rad init --full` with Azure Provider - Service Principal", full: true, azureProvider: &azure.Provider{ SubscriptionID: "test-subscription", ResourceGroup: "test-rg", - ServicePrincipal: &azure.ServicePrincipal{ + CredentialKind: "ServicePrincipal", + ServicePrincipal: &azure.ServicePrincipalCredential{ TenantID: "test-tenantId", ClientID: "test-clientId", ClientSecret: "test-clientSecret", @@ -624,6 +671,22 @@ func Test_Run_InstallAndCreateEnvironment(t *testing.T) { recipes: nil, expectedOutput: []any{}, }, + { + name: "`rad init --full` with Azure Provider - Workload Identity", + full: true, + azureProvider: &azure.Provider{ + SubscriptionID: "test-subscription", + ResourceGroup: "test-rg", + CredentialKind: "WorkloadIdentity", + WorkloadIdentity: &azure.WorkloadIdentityCredential{ + TenantID: "test-tenantId", + ClientID: "test-clientId", + }, + }, + awsProvider: nil, + recipes: nil, + expectedOutput: []any{}, + }, { name: "`rad init` with AWS Provider", full: false, @@ -1095,8 +1158,29 @@ func setAzureServicePrincipalTenantIDPrompt(prompter *prompt.MockInterface, tena Times(1) } -// setAzureCloudProvider sets up mocks that will configure an Azure cloud provider. -func setAzureCloudProvider(prompter *prompt.MockInterface, client *azure.MockClient, provider azure.Provider) { +func setAzureWorkloadIdentityAppIDPrompt(prompter *prompt.MockInterface, appID string) { + prompter.EXPECT(). + GetTextInput(enterAzureWorkloadIdentityAppIDPrompt, gomock.Any()). + Return(appID, nil). + Times(1) +} + +func setAzureWorkloadIdentityTenantIDPrompt(prompter *prompt.MockInterface, tenantID string) { + prompter.EXPECT(). + GetTextInput(enterAzureWorkloadIdentityTenantIDPrompt, gomock.Any()). + Return(tenantID, nil). + Times(1) +} + +func setAzureCredentialKindPrompt(prompter *prompt.MockInterface, choice string) { + prompter.EXPECT(). + GetListInput([]string{"Service Principal", "Workload Identity"}, selectAzureCredentialKindPrompt). + Return(choice, nil). + Times(1) +} + +// setAzureCloudProviderServicePrincipal sets up mocks that will configure an Azure cloud provider with service principal credential. +func setAzureCloudProviderServicePrincipal(prompter *prompt.MockInterface, client *azure.MockClient, provider azure.Provider) { subscriptions := &azure.SubscriptionResult{ Subscriptions: []azure.Subscription{{ID: provider.SubscriptionID, Name: "test-subscription"}}, } @@ -1110,11 +1194,34 @@ func setAzureCloudProvider(prompter *prompt.MockInterface, client *azure.MockCli setAzureResourceGroups(client, provider.SubscriptionID, resourceGroups) setAzureResourceGroupPrompt(prompter, []string{provider.ResourceGroup}, provider.ResourceGroup) + setAzureCredentialKindPrompt(prompter, "Service Principal") + setAzureServicePrincipalAppIDPrompt(prompter, provider.ServicePrincipal.ClientID) setAzureServicePrincipalPasswordPrompt(prompter, provider.ServicePrincipal.ClientSecret) setAzureServicePrincipalTenantIDPrompt(prompter, provider.ServicePrincipal.TenantID) } +// setAzureCloudProviderWorkloadIdentity sets up mocks that will configure an Azure cloud provider with workload identity credential. +func setAzureCloudProviderWorkloadIdentity(prompter *prompt.MockInterface, client *azure.MockClient, provider azure.Provider) { + subscriptions := &azure.SubscriptionResult{ + Subscriptions: []azure.Subscription{{ID: provider.SubscriptionID, Name: "test-subscription"}}, + } + subscriptions.Default = &subscriptions.Subscriptions[0] + resourceGroups := []armresources.ResourceGroup{{Name: to.Ptr(provider.ResourceGroup)}} + + setAzureSubscriptions(client, subscriptions) + setAzureSubscriptionConfirmPrompt(prompter, subscriptions.Default.Name, prompt.ConfirmYes) + + setAzureResourceGroupCreatePrompt(prompter, prompt.ConfirmNo) + setAzureResourceGroups(client, provider.SubscriptionID, resourceGroups) + setAzureResourceGroupPrompt(prompter, []string{provider.ResourceGroup}, provider.ResourceGroup) + + setAzureCredentialKindPrompt(prompter, "Workload Identity") + + setAzureWorkloadIdentityAppIDPrompt(prompter, provider.WorkloadIdentity.ClientID) + setAzureWorkloadIdentityTenantIDPrompt(prompter, provider.WorkloadIdentity.TenantID) +} + func setConfirmOption(prompter *prompt.MockInterface, choice summaryResult) { prompter.EXPECT(). RunProgram(gomock.Any()). diff --git a/pkg/cli/cmd/radinit/options.go b/pkg/cli/cmd/radinit/options.go index dc98d83d1d..6c3c981c0a 100644 --- a/pkg/cli/cmd/radinit/options.go +++ b/pkg/cli/cmd/radinit/options.go @@ -32,6 +32,8 @@ type initOptions struct { CloudProviders cloudProviderOptions Recipes recipePackOptions Application applicationOptions + // SetValues is a list of values that will be passed to Helm when installing the application. + SetValues []string } // clusterOptions holds all of the options that will be used to initialize the Kubernetes cluster. diff --git a/pkg/cli/credential/azure_credential_management.go b/pkg/cli/credential/azure_credential_management.go index 4070a41a15..da206fb216 100644 --- a/pkg/cli/credential/azure_credential_management.go +++ b/pkg/cli/credential/azure_credential_management.go @@ -45,39 +45,40 @@ type AzureCredentialManagementClientInterface interface { Delete(ctx context.Context, name string) (bool, error) } -const ( - AzureCredential = "azure" - AzurePlaneName = "azurecloud" - azureCredentialKind = "ServicePrincipal" -) +// AzureCredentialProperties is the representation of an Azure credential. +// It contains the kind of the credential (ServicePrincipal or WorkloadIdentity) and the properties for each kind. +type AzureCredentialProperties struct { + // Kind is the credential kind (ServicePrincipal or WorkloadIdentity) + Kind *string -// CloudProviderStatus is the representation of a cloud provider configuration. -type CloudProviderStatus struct { - // Name is the name/kind of the provider. For right now this only supports Azure and AWS. - Name string + // ServicePrincipal is the properties for an Azure service principal credential + ServicePrincipal *AzureServicePrincipalCredentialProperties - // Enabled is the enabled/disabled status of the provider. - Enabled bool + // WorkloadIdentity is the properties for an Azure workload identity credential + WorkloadIdentity *AzureWorkloadIdentityCredentialProperties } -type ProviderCredentialConfiguration struct { - CloudProviderStatus +// AzureServicePrincipalCredentialProperties is the representation of an Azure service principal credential. +type AzureServicePrincipalCredentialProperties struct { + // clientId for the Azure credential + ClientID *string - // AzureCredentials is used to set the credentials on Puts. It is NOT returned on Get/List. - AzureCredentials *AzureCredentialProperties + // kind for the Azure credential (must be ServicePrincipal) + Kind *string - // AWSCredentials is used to set the credentials on Puts. It is NOT returned on Get/List. - AWSCredentials *AWSCredentialProperties + // tenantId for the Azure credential + TenantID *string } -type AzureCredentialProperties struct { - // clientId for ServicePrincipal +// AzureWorkloadIdentityCredentialProperties is the representation of an Azure workload identity credential. +type AzureWorkloadIdentityCredentialProperties struct { + // clientId for the Azure credential ClientID *string - // The credential kind + // kind for the Azure credential (must be WorkloadIdentity) Kind *string - // tenantId for ServicePrincipal + // tenantId for the Azure credential TenantID *string } @@ -124,24 +125,59 @@ func (cpm *AzureCredentialManagementClient) Get(ctx context.Context, credentialN return ProviderCredentialConfiguration{}, err } - azureServicePrincipal, ok := resp.AzureCredentialResource.Properties.(*ucp.AzureServicePrincipalProperties) + azureCredential, ok := resp.AzureCredentialResource.Properties.(*ucp.AzureCredentialProperties) if !ok { return ProviderCredentialConfiguration{}, clierrors.Message("Unable to find credentials for cloud provider %s.", AzureCredential) } - providerCredentialConfiguration := ProviderCredentialConfiguration{ - CloudProviderStatus: CloudProviderStatus{ - Name: AzureCredential, - Enabled: true, - }, - AzureCredentials: &AzureCredentialProperties{ - ClientID: azureServicePrincipal.ClientID, - Kind: (*string)(azureServicePrincipal.Kind), - TenantID: azureServicePrincipal.TenantID, - }, - } + azureCredentialKind := azureCredential.GetAzureCredentialProperties().Kind - return providerCredentialConfiguration, nil + switch *azureCredentialKind { + case ucp.AzureCredentialKindServicePrincipal: + azureServicePrincipal, ok := resp.AzureCredentialResource.Properties.(*ucp.AzureServicePrincipalProperties) + if !ok { + return ProviderCredentialConfiguration{}, clierrors.Message("Unable to find credentials for cloud provider %s.", AzureCredential) + } + + providerCredentialConfiguration := ProviderCredentialConfiguration{ + CloudProviderStatus: CloudProviderStatus{ + Name: AzureCredential, + Enabled: true, + }, + AzureCredentials: &AzureCredentialProperties{ + ServicePrincipal: &AzureServicePrincipalCredentialProperties{ + ClientID: azureServicePrincipal.ClientID, + Kind: (*string)(azureServicePrincipal.Kind), + TenantID: azureServicePrincipal.TenantID, + }, + }, + } + + return providerCredentialConfiguration, nil + case ucp.AzureCredentialKindWorkloadIdentity: + azureWorkloadIdentity, ok := resp.AzureCredentialResource.Properties.(*ucp.AzureWorkloadIdentityProperties) + if !ok { + return ProviderCredentialConfiguration{}, clierrors.Message("Unable to find credentials for cloud provider %s.", AzureCredential) + } + + providerCredentialConfiguration := ProviderCredentialConfiguration{ + CloudProviderStatus: CloudProviderStatus{ + Name: AzureCredential, + Enabled: true, + }, + AzureCredentials: &AzureCredentialProperties{ + WorkloadIdentity: &AzureWorkloadIdentityCredentialProperties{ + ClientID: azureWorkloadIdentity.ClientID, + Kind: (*string)(azureWorkloadIdentity.Kind), + TenantID: azureWorkloadIdentity.TenantID, + }, + }, + } + + return providerCredentialConfiguration, nil + default: + return ProviderCredentialConfiguration{}, clierrors.Message("Unable to find credentials for cloud provider %s.", AzureCredential) + } } // List, lists the credentials registered with all ucp provider planes diff --git a/pkg/cli/credential/credential_management.go b/pkg/cli/credential/credential_management.go index 41b602fe35..9eceb6f857 100644 --- a/pkg/cli/credential/credential_management.go +++ b/pkg/cli/credential/credential_management.go @@ -25,8 +25,11 @@ import ( ) const ( - AzurePlaneType = "azure" - AWSPlaneType = "aws" + AzurePlaneType = "azure" + AWSPlaneType = "aws" + AzureCredential = "azure" + AzurePlaneName = "azurecloud" + defaultSecretName = "default" ) //go:generate mockgen -typed -destination=./mock_credentialmanagementclient.go -package=credential -self_package github.com/radius-project/radius/pkg/cli/credential github.com/radius-project/radius/pkg/cli/credential CredentialManagementClient @@ -45,9 +48,24 @@ type CredentialManagementClient interface { Delete(ctx context.Context, providerName string) (bool, error) } -const ( - defaultSecretName = "default" -) +// CloudProviderStatus is the representation of a cloud provider configuration. +type CloudProviderStatus struct { + // Name is the name/kind of the provider. For right now this only supports Azure and AWS. + Name string + + // Enabled is the enabled/disabled status of the provider. + Enabled bool +} + +type ProviderCredentialConfiguration struct { + CloudProviderStatus + + // AzureCredentials is used to set the credentials on Puts. It is NOT returned on Get/List. + AzureCredentials *AzureCredentialProperties + + // AWSCredentials is used to set the credentials on Puts. It is NOT returned on Get/List. + AWSCredentials *AWSCredentialProperties +} // UCPCredentialManagementClient implements operations to manage credentials on ucp. type UCPCredentialManagementClient struct { diff --git a/pkg/recipes/terraform/config/providers/azure.go b/pkg/recipes/terraform/config/providers/azure.go index 6880fe339d..ba1ad0be6c 100644 --- a/pkg/recipes/terraform/config/providers/azure.go +++ b/pkg/recipes/terraform/config/providers/azure.go @@ -26,6 +26,7 @@ import ( "github.com/radius-project/radius/pkg/recipes" "github.com/radius-project/radius/pkg/sdk" "github.com/radius-project/radius/pkg/ucp/credentials" + ucp_datamodel "github.com/radius-project/radius/pkg/ucp/datamodel" "github.com/radius-project/radius/pkg/ucp/resources" resources_azure "github.com/radius-project/radius/pkg/ucp/resources/azure" "github.com/radius-project/radius/pkg/ucp/secret" @@ -38,11 +39,13 @@ import ( const ( AzureProviderName = "azurerm" - azureFeaturesParam = "features" - azureSubIDParam = "subscription_id" - azureClientIDParam = "client_id" - azureClientSecretParam = "client_secret" - azureTenantIDParam = "tenant_id" + azureFeaturesParam = "features" + azureSubIDParam = "subscription_id" + azureClientIDParam = "client_id" + azureClientSecretParam = "client_secret" + azureTenantIDParam = "tenant_id" + azureUseAKSWorkloadIdentityParam = "use_aks_workload_identity" + azureUseCLIParam = "use_cli" ) var _ Provider = (*azureProvider)(nil) @@ -119,19 +122,38 @@ func fetchAzureCredentials(ctx context.Context, azureCredentialsProvider credent credentials, err := azureCredentialsProvider.Fetch(ctx, credentials.AzureCloud, "default") if err != nil { if errors.Is(err, &secret.ErrNotFound{}) { - logger.Info("AWS credentials are not registered, skipping credentials configuration.") + logger.Info("Azure credentials are not registered, skipping credentials configuration.") return nil, nil } return nil, err } - if credentials == nil || credentials.ClientID == "" || credentials.TenantID == "" || credentials.ClientSecret == "" { - logger.Info("Azure credentials are not registered, skipping credentials configuration.") + switch credentials.Kind { + case ucp_datamodel.AzureServicePrincipalCredentialKind: + if credentials.ServicePrincipal == nil || + credentials.ServicePrincipal.ClientID == "" || + credentials.ServicePrincipal.TenantID == "" || + credentials.ServicePrincipal.ClientSecret == "" { + logger.Info("Azure service principal credentials are not registered, skipping credentials configuration.") + return nil, nil + } + + return credentials, nil + case ucp_datamodel.AzureWorkloadIdentityCredentialKind: + if credentials.WorkloadIdentity == nil || + credentials.WorkloadIdentity.ClientID == "" || + credentials.WorkloadIdentity.TenantID == "" { + logger.Info("Azure workload identity credentials are not registered, skipping credentials configuration.") + return nil, nil + } + + return credentials, nil + default: + logger.Info("Azure credential is not supported, skipping credentials configuration, kind: %s", credentials.Kind) return nil, nil } - return credentials, nil } func (p *azureProvider) generateProviderConfigMap(configMap map[string]any, credentials *credentials.AzureCredential, subscriptionID string) map[string]any { @@ -139,10 +161,28 @@ func (p *azureProvider) generateProviderConfigMap(configMap map[string]any, cred configMap[azureSubIDParam] = subscriptionID } - if credentials != nil && credentials.ClientID != "" && credentials.TenantID != "" && credentials.ClientSecret != "" { - configMap[azureClientIDParam] = credentials.ClientID - configMap[azureClientSecretParam] = credentials.ClientSecret - configMap[azureTenantIDParam] = credentials.TenantID + switch credentials.Kind { + case ucp_datamodel.AzureServicePrincipalCredentialKind: + if credentials.ServicePrincipal != nil && + credentials.ServicePrincipal.ClientID != "" && + credentials.ServicePrincipal.TenantID != "" && + credentials.ServicePrincipal.ClientSecret != "" { + configMap[azureClientIDParam] = credentials.ServicePrincipal.ClientID + configMap[azureClientSecretParam] = credentials.ServicePrincipal.ClientSecret + configMap[azureTenantIDParam] = credentials.ServicePrincipal.TenantID + } + case ucp_datamodel.AzureWorkloadIdentityCredentialKind: + if credentials.WorkloadIdentity != nil && + credentials.WorkloadIdentity.ClientID != "" && + credentials.WorkloadIdentity.TenantID != "" { + configMap[azureClientIDParam] = credentials.WorkloadIdentity.ClientID + configMap[azureTenantIDParam] = credentials.WorkloadIdentity.TenantID + + // Use AKS Workload Identity for Azure provider + // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/aks_workload_identity#configuring-with-environment-variables + configMap[azureUseAKSWorkloadIdentityParam] = true + configMap[azureUseCLIParam] = false + } } return configMap diff --git a/pkg/recipes/terraform/config/providers/azure_test.go b/pkg/recipes/terraform/config/providers/azure_test.go index 9ba20ddc91..fbfeb90ff1 100644 --- a/pkg/recipes/terraform/config/providers/azure_test.go +++ b/pkg/recipes/terraform/config/providers/azure_test.go @@ -31,11 +31,21 @@ import ( ) var ( - testSubscription = "test-sub" - testAzureCredentials = ucp_credentials.AzureCredential{ - TenantID: "testTenantID", - ClientSecret: "testClientSecret", - ClientID: "testClientID", + testSubscription = "test-sub" + testAzureWorkloadIdentityCredential = ucp_credentials.AzureCredential{ + Kind: "WorkloadIdentity", + WorkloadIdentity: &ucp_credentials.AzureWorkloadIdentityCredential{ + TenantID: "testTenantID", + ClientID: "testClientID", + }, + } + testAzureServicePrincipalCredential = ucp_credentials.AzureCredential{ + Kind: "ServicePrincipal", + ServicePrincipal: &ucp_credentials.AzureServicePrincipalCredential{ + TenantID: "testTenantID", + ClientSecret: "testClientSecret", + ClientID: "testClientID", + }, } ) @@ -43,12 +53,27 @@ type mockAzureCredentialsProvider struct { testCredential *ucp_credentials.AzureCredential } -func newMockAzureCredentialsProvider() *mockAzureCredentialsProvider { +func newMockAzureServicePrincipalCredentialsProvider() *mockAzureCredentialsProvider { + return &mockAzureCredentialsProvider{ + testCredential: &ucp_credentials.AzureCredential{ + Kind: "ServicePrincipal", + ServicePrincipal: &ucp_credentials.AzureServicePrincipalCredential{ + ClientID: "testClientID", + TenantID: "testTenantID", + ClientSecret: "testClientSecret", + }, + }, + } +} + +func newMockAzureWorkloadIdentityCredentialsProvider() *mockAzureCredentialsProvider { return &mockAzureCredentialsProvider{ testCredential: &ucp_credentials.AzureCredential{ - TenantID: testAzureCredentials.TenantID, - ClientSecret: testAzureCredentials.ClientSecret, - ClientID: testAzureCredentials.ClientID, + Kind: "WorkloadIdentity", + WorkloadIdentity: &ucp_credentials.AzureWorkloadIdentityCredential{ + ClientID: "testClientID", + TenantID: "testTenantID", + }, }, } } @@ -60,12 +85,23 @@ func (p *mockAzureCredentialsProvider) Fetch(ctx context.Context, planeName, nam return nil, &secret.ErrNotFound{} } - if p.testCredential.TenantID == "" && p.testCredential.ClientID == "" && p.testCredential.ClientSecret == "" { - return p.testCredential, nil - } + switch (*p.testCredential).Kind { + case "ServicePrincipal": + if p.testCredential.ServicePrincipal.TenantID == "" && p.testCredential.ServicePrincipal.ClientID == "" && p.testCredential.ServicePrincipal.ClientSecret == "" { + return p.testCredential, nil + } + + if p.testCredential.ServicePrincipal.TenantID == "" { + return nil, errors.New("failed to fetch credential") + } + case "WorkloadIdentity": + if p.testCredential.WorkloadIdentity.TenantID == "" && p.testCredential.WorkloadIdentity.ClientID == "" { + return p.testCredential, nil + } - if p.testCredential.TenantID == "" { - return nil, errors.New("failed to fetch credential") + if p.testCredential.WorkloadIdentity.TenantID == "" { + return nil, errors.New("failed to fetch credential") + } } return p.testCredential, nil @@ -187,9 +223,15 @@ func TestAzureProvider_FetchCredentials(t *testing.T) { expectedErr bool }{ { - desc: "valid credentials", - credentialsProvider: newMockAzureCredentialsProvider(), - expectedCreds: &testAzureCredentials, + desc: "valid credentials serviceprincipal", + credentialsProvider: newMockAzureServicePrincipalCredentialsProvider(), + expectedCreds: &testAzureServicePrincipalCredential, + expectedErr: false, + }, + { + desc: "valid credentials workloadidentity", + credentialsProvider: newMockAzureWorkloadIdentityCredentialsProvider(), + expectedCreds: &testAzureWorkloadIdentityCredential, expectedErr: false, }, { @@ -204,9 +246,12 @@ func TestAzureProvider_FetchCredentials(t *testing.T) { desc: "empty values - no error", credentialsProvider: &mockAzureCredentialsProvider{ &ucp_credentials.AzureCredential{ - TenantID: "", - ClientID: "", - ClientSecret: "", + Kind: "ServicePrincipal", + ServicePrincipal: &ucp_credentials.AzureServicePrincipalCredential{ + TenantID: "", + ClientID: "", + ClientSecret: "", + }, }, }, expectedCreds: nil, @@ -216,9 +261,12 @@ func TestAzureProvider_FetchCredentials(t *testing.T) { desc: "fetch credential error", credentialsProvider: &mockAzureCredentialsProvider{ &ucp_credentials.AzureCredential{ - TenantID: "", - ClientID: testAzureCredentials.ClientID, - ClientSecret: testAzureCredentials.ClientSecret, + Kind: "ServicePrincipal", + ServicePrincipal: &ucp_credentials.AzureServicePrincipalCredential{ + TenantID: "", + ClientID: testAzureServicePrincipalCredential.ServicePrincipal.ClientID, + ClientSecret: testAzureServicePrincipalCredential.ServicePrincipal.ClientSecret, + }, }, }, expectedCreds: nil, @@ -250,25 +298,38 @@ func TestAzureProvider_generateProviderConfigMap(t *testing.T) { expectedConfig map[string]any }{ { - desc: "valid config", + desc: "valid config - serviceprincipal", subscription: testSubscription, - credentials: testAzureCredentials, + credentials: testAzureServicePrincipalCredential, expectedConfig: map[string]any{ azureFeaturesParam: map[string]any{}, azureSubIDParam: testSubscription, - azureTenantIDParam: testAzureCredentials.TenantID, - azureClientIDParam: testAzureCredentials.ClientID, - azureClientSecretParam: testAzureCredentials.ClientSecret, + azureTenantIDParam: testAzureServicePrincipalCredential.ServicePrincipal.TenantID, + azureClientIDParam: testAzureServicePrincipalCredential.ServicePrincipal.ClientID, + azureClientSecretParam: testAzureServicePrincipalCredential.ServicePrincipal.ClientSecret, + }, + }, + { + desc: "valid config - workloadidentity", + subscription: testSubscription, + credentials: testAzureWorkloadIdentityCredential, + expectedConfig: map[string]any{ + azureFeaturesParam: map[string]any{}, + azureSubIDParam: testSubscription, + azureTenantIDParam: testAzureWorkloadIdentityCredential.WorkloadIdentity.TenantID, + azureClientIDParam: testAzureWorkloadIdentityCredential.WorkloadIdentity.ClientID, + azureUseAKSWorkloadIdentityParam: true, + azureUseCLIParam: false, }, }, { desc: "missing subscription", - credentials: testAzureCredentials, + credentials: testAzureServicePrincipalCredential, expectedConfig: map[string]any{ azureFeaturesParam: map[string]any{}, - azureTenantIDParam: testAzureCredentials.TenantID, - azureClientIDParam: testAzureCredentials.ClientID, - azureClientSecretParam: testAzureCredentials.ClientSecret, + azureTenantIDParam: testAzureServicePrincipalCredential.ServicePrincipal.TenantID, + azureClientIDParam: testAzureServicePrincipalCredential.ServicePrincipal.ClientID, + azureClientSecretParam: testAzureServicePrincipalCredential.ServicePrincipal.ClientSecret, }, }, { @@ -282,9 +343,12 @@ func TestAzureProvider_generateProviderConfigMap(t *testing.T) { { desc: "invalid credentials", credentials: ucp_credentials.AzureCredential{ - TenantID: "", - ClientID: testAzureCredentials.ClientID, - ClientSecret: testAzureCredentials.ClientSecret, + Kind: "ServicePrincipal", + ServicePrincipal: &ucp_credentials.AzureServicePrincipalCredential{ + TenantID: "", + ClientID: testAzureServicePrincipalCredential.ServicePrincipal.ClientID, + ClientSecret: testAzureServicePrincipalCredential.ServicePrincipal.ClientSecret, + }, }, expectedConfig: map[string]any{ azureFeaturesParam: map[string]any{}, diff --git a/pkg/ucp/api/v20231001preview/azure_credential_conversion.go b/pkg/ucp/api/v20231001preview/azure_credential_conversion.go index 71af6a3c7f..c0ec2187af 100644 --- a/pkg/ucp/api/v20231001preview/azure_credential_conversion.go +++ b/pkg/ucp/api/v20231001preview/azure_credential_conversion.go @@ -82,11 +82,45 @@ func (cr *AzureCredentialResource) getDataModelCredentialProperties() (*datamode } return &datamodel.AzureCredentialResourceProperties{ - Kind: datamodel.AzureCredentialKind, + Kind: datamodel.AzureServicePrincipalCredentialKind, AzureCredential: &datamodel.AzureCredentialProperties{ - TenantID: to.String(p.TenantID), - ClientID: to.String(p.ClientID), - ClientSecret: to.String(p.ClientSecret), + Kind: datamodel.AzureServicePrincipalCredentialKind, + ServicePrincipal: &datamodel.AzureServicePrincipalCredentialProperties{ + TenantID: to.String(p.TenantID), + ClientID: to.String(p.ClientID), + ClientSecret: to.String(p.ClientSecret), + }, + }, + Storage: storage, + }, nil + case *AzureWorkloadIdentityProperties: + var storage *datamodel.CredentialStorageProperties + + switch c := p.Storage.(type) { + case *InternalCredentialStorageProperties: + if c.Kind == nil { + return nil, &v1.ErrModelConversion{PropertyName: "$.properties", ValidValue: "not nil"} + } + storage = &datamodel.CredentialStorageProperties{ + Kind: datamodel.InternalStorageKind, + InternalCredential: &datamodel.InternalCredentialStorageProperties{ + SecretName: to.String(c.SecretName), + }, + } + case nil: + return nil, &v1.ErrModelConversion{PropertyName: "$.properties.storage", ValidValue: "not nil"} + default: + return nil, &v1.ErrModelConversion{PropertyName: "$.properties.storage.kind", ValidValue: fmt.Sprintf("one of %q", PossibleCredentialStorageKindValues())} + } + + return &datamodel.AzureCredentialResourceProperties{ + Kind: datamodel.AzureWorkloadIdentityCredentialKind, + AzureCredential: &datamodel.AzureCredentialProperties{ + Kind: datamodel.AzureWorkloadIdentityCredentialKind, + WorkloadIdentity: &datamodel.AzureWorkloadIdentityCredentialProperties{ + TenantID: to.String(p.TenantID), + ClientID: to.String(p.ClientID), + }, }, Storage: storage, }, nil @@ -121,11 +155,24 @@ func (dst *AzureCredentialResource) ConvertFrom(src v1.DataModelInterface) error // DO NOT convert any secret values to versioned model. switch dm.Properties.Kind { - case datamodel.AzureCredentialKind: + case datamodel.AzureServicePrincipalCredentialKind: + if dm.Properties.AzureCredential.ServicePrincipal == nil { + return v1.ErrInvalidModelConversion + } dst.Properties = &AzureServicePrincipalProperties{ Kind: to.Ptr(AzureCredentialKind(dm.Properties.Kind)), - ClientID: to.Ptr(dm.Properties.AzureCredential.ClientID), - TenantID: to.Ptr(dm.Properties.AzureCredential.TenantID), + ClientID: to.Ptr(dm.Properties.AzureCredential.ServicePrincipal.ClientID), + TenantID: to.Ptr(dm.Properties.AzureCredential.ServicePrincipal.TenantID), + Storage: storage, + } + case datamodel.AzureWorkloadIdentityCredentialKind: + if dm.Properties.AzureCredential.WorkloadIdentity == nil { + return v1.ErrInvalidModelConversion + } + dst.Properties = &AzureWorkloadIdentityProperties{ + Kind: to.Ptr(AzureCredentialKind(dm.Properties.Kind)), + ClientID: to.Ptr(dm.Properties.AzureCredential.WorkloadIdentity.ClientID), + TenantID: to.Ptr(dm.Properties.AzureCredential.WorkloadIdentity.TenantID), Storage: storage, } default: diff --git a/pkg/ucp/api/v20231001preview/azure_credential_conversion_test.go b/pkg/ucp/api/v20231001preview/azure_credential_conversion_test.go index c598f3f6af..d244c70a20 100644 --- a/pkg/ucp/api/v20231001preview/azure_credential_conversion_test.go +++ b/pkg/ucp/api/v20231001preview/azure_credential_conversion_test.go @@ -36,7 +36,7 @@ func TestAzureCredentialConvertVersionedToDataModel(t *testing.T) { err error }{ { - filename: "credentialresource-azure.json", + filename: "credentialresource-azure-serviceprincipal.json", expected: &datamodel.AzureCredential{ BaseResource: v1.BaseResource{ TrackedResource: v1.TrackedResource{ @@ -55,9 +55,45 @@ func TestAzureCredentialConvertVersionedToDataModel(t *testing.T) { Properties: &datamodel.AzureCredentialResourceProperties{ Kind: "ServicePrincipal", AzureCredential: &datamodel.AzureCredentialProperties{ - TenantID: "00000000-0000-0000-0000-000000000000", - ClientID: "00000000-0000-0000-0000-000000000000", - ClientSecret: "secret", + Kind: datamodel.AzureServicePrincipalCredentialKind, + ServicePrincipal: &datamodel.AzureServicePrincipalCredentialProperties{ + TenantID: "00000000-0000-0000-0000-000000000000", + ClientID: "00000000-0000-0000-0000-000000000000", + ClientSecret: "secret", + }, + }, + Storage: &datamodel.CredentialStorageProperties{ + Kind: datamodel.InternalStorageKind, + InternalCredential: &datamodel.InternalCredentialStorageProperties{}, + }, + }, + }, + }, + { + filename: "credentialresource-azure-workloadidentity.json", + expected: &datamodel.AzureCredential{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + ID: "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + Name: "default", + Type: "System.Azure/credentials", + Location: "west-us-2", + Tags: map[string]string{ + "env": "dev", + }, + }, + InternalMetadata: v1.InternalMetadata{ + UpdatedAPIVersion: Version, + }, + }, + Properties: &datamodel.AzureCredentialResourceProperties{ + Kind: "WorkloadIdentity", + AzureCredential: &datamodel.AzureCredentialProperties{ + Kind: datamodel.AzureWorkloadIdentityCredentialKind, + WorkloadIdentity: &datamodel.AzureWorkloadIdentityCredentialProperties{ + TenantID: "00000000-0000-0000-0000-000000000000", + ClientID: "00000000-0000-0000-0000-000000000000", + }, }, Storage: &datamodel.CredentialStorageProperties{ Kind: datamodel.InternalStorageKind, @@ -114,7 +150,7 @@ func TestAzureCredentialConvertDataModelToVersioned(t *testing.T) { err error }{ { - filename: "credentialresourcedatamodel-azure.json", + filename: "credentialresourcedatamodel-azure-serviceprincipal.json", expected: &AzureCredentialResource{ ID: to.Ptr("/planes/azure/azurecloud/providers/System.Azure/credentials/default"), Name: to.Ptr("default"), @@ -134,6 +170,27 @@ func TestAzureCredentialConvertDataModelToVersioned(t *testing.T) { }, }, }, + { + filename: "credentialresourcedatamodel-azure-workloadidentity.json", + expected: &AzureCredentialResource{ + ID: to.Ptr("/planes/azure/azurecloud/providers/System.Azure/credentials/default"), + Name: to.Ptr("default"), + Type: to.Ptr("System.Azure/credentials"), + Location: to.Ptr("west-us-2"), + Tags: map[string]*string{ + "env": to.Ptr("dev"), + }, + Properties: &AzureWorkloadIdentityProperties{ + Kind: to.Ptr(AzureCredentialKindWorkloadIdentity), + ClientID: to.Ptr("00000000-0000-0000-0000-000000000000"), + TenantID: to.Ptr("00000000-0000-0000-0000-000000000000"), + Storage: &InternalCredentialStorageProperties{ + Kind: to.Ptr(CredentialStorageKindInternal), + SecretName: to.Ptr("azure-azurecloud-default"), + }, + }, + }, + }, { filename: "credentialresourcedatamodel-default.json", err: v1.ErrInvalidModelConversion, diff --git a/pkg/ucp/api/v20231001preview/testdata/credentialresource-azure.json b/pkg/ucp/api/v20231001preview/testdata/credentialresource-azure-serviceprincipal.json similarity index 100% rename from pkg/ucp/api/v20231001preview/testdata/credentialresource-azure.json rename to pkg/ucp/api/v20231001preview/testdata/credentialresource-azure-serviceprincipal.json diff --git a/pkg/ucp/api/v20231001preview/testdata/credentialresource-azure-workloadidentity.json b/pkg/ucp/api/v20231001preview/testdata/credentialresource-azure-workloadidentity.json new file mode 100644 index 0000000000..5d6d681a75 --- /dev/null +++ b/pkg/ucp/api/v20231001preview/testdata/credentialresource-azure-workloadidentity.json @@ -0,0 +1,17 @@ +{ + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "tags": { + "env": "dev" + }, + "properties": { + "kind": "WorkloadIdentity", + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } +} \ No newline at end of file diff --git a/pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure.json b/pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure-serviceprincipal.json similarity index 76% rename from pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure.json rename to pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure-serviceprincipal.json index 6bee5560b5..d597ff9bc5 100644 --- a/pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure.json +++ b/pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure-serviceprincipal.json @@ -18,9 +18,12 @@ "namespace": "radius-system", "kind": "ServicePrincipal", "azureCredential": { - "tenantId": "00000000-0000-0000-0000-000000000000", - "clientId": "00000000-0000-0000-0000-000000000000", - "secret": "secret" + "kind": "ServicePrincipal", + "servicePrincipal": { + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "secret": "secret" + } }, "storage": { "kind": "Internal", diff --git a/pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure-workloadidentity.json b/pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure-workloadidentity.json new file mode 100644 index 0000000000..bde5380eff --- /dev/null +++ b/pkg/ucp/api/v20231001preview/testdata/credentialresourcedatamodel-azure-workloadidentity.json @@ -0,0 +1,34 @@ +{ + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "systemData": { + "createdBy": "fakeid@live.com", + "createdByType": "User", + "createdAt": "2021-09-24T19:09:54.2403864Z", + "lastModifiedBy": "fakeid@live.com", + "lastModifiedByType": "User", + "lastModifiedAt": "2021-09-24T20:09:54.2403864Z" + }, + "tags": { + "env": "dev" + }, + "properties": { + "namespace": "radius-system", + "kind": "WorkloadIdentity", + "azureCredential": { + "kind": "WorkloadIdentity", + "workloadIdentity": { + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000" + } + }, + "storage": { + "kind": "Internal", + "internalCredential": { + "secretName": "azure-azurecloud-default" + } + } + } +} \ No newline at end of file diff --git a/pkg/ucp/api/v20231001preview/zz_generated_constants.go b/pkg/ucp/api/v20231001preview/zz_generated_constants.go index ac10c48a0a..3818bcaa71 100644 --- a/pkg/ucp/api/v20231001preview/zz_generated_constants.go +++ b/pkg/ucp/api/v20231001preview/zz_generated_constants.go @@ -33,12 +33,15 @@ type AzureCredentialKind string const ( // AzureCredentialKindServicePrincipal - The Service Principal Credential AzureCredentialKindServicePrincipal AzureCredentialKind = "ServicePrincipal" + // AzureCredentialKindWorkloadIdentity - The Workload Identity Credential + AzureCredentialKindWorkloadIdentity AzureCredentialKind = "WorkloadIdentity" ) // PossibleAzureCredentialKindValues returns the possible values for the AzureCredentialKind const type. func PossibleAzureCredentialKindValues() []AzureCredentialKind { return []AzureCredentialKind{ AzureCredentialKindServicePrincipal, + AzureCredentialKindWorkloadIdentity, } } diff --git a/pkg/ucp/api/v20231001preview/zz_generated_interfaces.go b/pkg/ucp/api/v20231001preview/zz_generated_interfaces.go index ab611a9e3a..72b5581ffd 100644 --- a/pkg/ucp/api/v20231001preview/zz_generated_interfaces.go +++ b/pkg/ucp/api/v20231001preview/zz_generated_interfaces.go @@ -19,7 +19,7 @@ type AwsCredentialPropertiesClassification interface { // AzureCredentialPropertiesClassification provides polymorphic access to related types. // Call the interface's GetAzureCredentialProperties() method to access the common type. // Use a type switch to determine the concrete type. The possible types are: -// - *AzureCredentialProperties, *AzureServicePrincipalProperties +// - *AzureCredentialProperties, *AzureServicePrincipalProperties, *AzureWorkloadIdentityProperties type AzureCredentialPropertiesClassification interface { // GetAzureCredentialProperties returns the AzureCredentialProperties content of the underlying type. GetAzureCredentialProperties() *AzureCredentialProperties diff --git a/pkg/ucp/api/v20231001preview/zz_generated_models.go b/pkg/ucp/api/v20231001preview/zz_generated_models.go index 564944e7f6..9a57b377e7 100644 --- a/pkg/ucp/api/v20231001preview/zz_generated_models.go +++ b/pkg/ucp/api/v20231001preview/zz_generated_models.go @@ -231,7 +231,7 @@ type AzurePlaneResourceTagsUpdate struct { Tags map[string]*string } -// AzureServicePrincipalProperties - The properties of Service Principal credential storage +// AzureServicePrincipalProperties - The properties of Azure Service Principal credential storage type AzureServicePrincipalProperties struct { // REQUIRED; clientId for ServicePrincipal ClientID *string @@ -260,6 +260,32 @@ func (a *AzureServicePrincipalProperties) GetAzureCredentialProperties() *AzureC } } +// AzureWorkloadIdentityProperties - The properties of Azure Workload Identity credential storage +type AzureWorkloadIdentityProperties struct { + // REQUIRED; clientId for WorkloadIdentity + ClientID *string + + // REQUIRED; The kind of Azure credential + Kind *AzureCredentialKind + + // REQUIRED; The storage properties + Storage CredentialStoragePropertiesClassification + + // REQUIRED; tenantId for WorkloadIdentity + TenantID *string + + // READ-ONLY; The status of the asynchronous operation. + ProvisioningState *ProvisioningState +} + +// GetAzureCredentialProperties implements the AzureCredentialPropertiesClassification interface for type AzureWorkloadIdentityProperties. +func (a *AzureWorkloadIdentityProperties) GetAzureCredentialProperties() *AzureCredentialProperties { + return &AzureCredentialProperties{ + Kind: a.Kind, + ProvisioningState: a.ProvisioningState, + } +} + // ComponentsKhmx01SchemasGenericresourceAllof0 - Concrete proxy resource types can be created by aliasing this type using // a specific property type. type ComponentsKhmx01SchemasGenericresourceAllof0 struct { diff --git a/pkg/ucp/api/v20231001preview/zz_generated_models_serde.go b/pkg/ucp/api/v20231001preview/zz_generated_models_serde.go index 794a7ffe1b..28932179ca 100644 --- a/pkg/ucp/api/v20231001preview/zz_generated_models_serde.go +++ b/pkg/ucp/api/v20231001preview/zz_generated_models_serde.go @@ -660,6 +660,49 @@ func (a *AzureServicePrincipalProperties) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaller interface for type AzureWorkloadIdentityProperties. +func (a AzureWorkloadIdentityProperties) MarshalJSON() ([]byte, error) { + objectMap := make(map[string]any) + populate(objectMap, "clientId", a.ClientID) + objectMap["kind"] = AzureCredentialKindWorkloadIdentity + populate(objectMap, "provisioningState", a.ProvisioningState) + populate(objectMap, "storage", a.Storage) + populate(objectMap, "tenantId", a.TenantID) + return json.Marshal(objectMap) +} + +// UnmarshalJSON implements the json.Unmarshaller interface for type AzureWorkloadIdentityProperties. +func (a *AzureWorkloadIdentityProperties) 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 "clientId": + err = unpopulate(val, "ClientID", &a.ClientID) + delete(rawMsg, key) + case "kind": + err = unpopulate(val, "Kind", &a.Kind) + delete(rawMsg, key) + case "provisioningState": + err = unpopulate(val, "ProvisioningState", &a.ProvisioningState) + delete(rawMsg, key) + case "storage": + a.Storage, err = unmarshalCredentialStoragePropertiesClassification(val) + delete(rawMsg, key) + case "tenantId": + err = unpopulate(val, "TenantID", &a.TenantID) + 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 ComponentsKhmx01SchemasGenericresourceAllof0. func (c ComponentsKhmx01SchemasGenericresourceAllof0) MarshalJSON() ([]byte, error) { objectMap := make(map[string]any) diff --git a/pkg/ucp/api/v20231001preview/zz_generated_polymorphic_helpers.go b/pkg/ucp/api/v20231001preview/zz_generated_polymorphic_helpers.go index 9cc824a3d4..4b05fb540c 100644 --- a/pkg/ucp/api/v20231001preview/zz_generated_polymorphic_helpers.go +++ b/pkg/ucp/api/v20231001preview/zz_generated_polymorphic_helpers.go @@ -42,6 +42,8 @@ func unmarshalAzureCredentialPropertiesClassification(rawMsg json.RawMessage) (A switch m["kind"] { case string(AzureCredentialKindServicePrincipal): b = &AzureServicePrincipalProperties{} + case string(AzureCredentialKindWorkloadIdentity): + b = &AzureWorkloadIdentityProperties{} default: b = &AzureCredentialProperties{} } diff --git a/pkg/ucp/aws/ucpcredentialprovider.go b/pkg/ucp/aws/ucpcredentialprovider.go index c77b8055dc..f72b1de6f7 100644 --- a/pkg/ucp/aws/ucpcredentialprovider.go +++ b/pkg/ucp/aws/ucpcredentialprovider.go @@ -77,7 +77,7 @@ func (c *UCPCredentialProvider) Retrieve(ctx context.Context) (aws.Credentials, return aws.Credentials{}, errors.New("invalid access key info") } - logger.Info(fmt.Sprintf("Retreived AWS Credential - AccessKeyID: %s", s.AccessKeyID)) + logger.Info(fmt.Sprintf("Retrieved AWS Credential - AccessKeyID: %s", s.AccessKeyID)) value := aws.Credentials{ AccessKeyID: s.AccessKeyID, diff --git a/pkg/ucp/credentials/azure.go b/pkg/ucp/credentials/azure.go index 10dc3b3d3e..de6e93b225 100644 --- a/pkg/ucp/credentials/azure.go +++ b/pkg/ucp/credentials/azure.go @@ -51,10 +51,10 @@ func NewAzureCredentialProvider(provider *provider.SecretProvider, ucpConn sdk.C }, nil } -// Fetch fetches the Azure service principal credentials from UCP and the internal storage (e.g. -// Kubernetes secret store) and returns an AzureCredential struct. If an error occurs, an error is returned. +// Fetch fetches the Azure credentials from UCP and the internal storage (e.g. Kubernetes secret store) +// and returns an AzureCredential struct. If an error occurs, an error is returned. func (p *AzureCredentialProvider) Fetch(ctx context.Context, planeName, name string) (*AzureCredential, error) { - // 1. Fetch the secret name of Azure service principal credentials from UCP. + // 1. Fetch the secret name of Azure credentials from UCP. cred, err := p.client.Get(ctx, planeName, name, &ucpapi.AzureCredentialsClientGetOptions{}) if err != nil { return nil, err @@ -69,10 +69,17 @@ func (p *AzureCredentialProvider) Fetch(ctx context.Context, planeName, name str case *ucpapi.InternalCredentialStorageProperties: storage = c default: - return nil, errors.New("invalid AzureServicePrincipalProperties") + return nil, errors.New("Azure Credential is invalid - field 'properties.storage' is not InternalCredentialStorageProperties") + } + case *ucpapi.AzureWorkloadIdentityProperties: + switch c := p.Storage.(type) { + case *ucpapi.InternalCredentialStorageProperties: + storage = c + default: + return nil, errors.New("Azure Credential is invalid - field 'properties.storage' is not InternalCredentialStorageProperties") } default: - return nil, errors.New("invalid InternalCredentialStorageProperties") + return nil, errors.New("Azure Credential is invalid - field 'properties' is not AzureServicePrincipalProperties or AzureWorkloadIdentityProperties") } secretName := to.String(storage.SecretName) diff --git a/pkg/ucp/credentials/types.go b/pkg/ucp/credentials/types.go index 2303bead61..8f2fdcb417 100644 --- a/pkg/ucp/credentials/types.go +++ b/pkg/ucp/credentials/types.go @@ -28,11 +28,21 @@ const ( // AWSPublic represents the aws public cloud plane name for UCP. AWSPublic = "aws" + + // AzureServicePrincipalCredentialKind represents the kind of Azure service principal credential. + AzureServicePrincipalCredentialKind = ucp_dm.AzureServicePrincipalCredentialKind + + // AzureWorkloadIdentityCredentialKind represents the kind of Azure workload identity credential. + AzureWorkloadIdentityCredentialKind = ucp_dm.AzureWorkloadIdentityCredentialKind ) type ( // AzureCredential represents a credential for Azure AD. AzureCredential = ucp_dm.AzureCredentialProperties + // AzureServicePrincipalCredential represents a credential for Azure AD service principal. + AzureServicePrincipalCredential = ucp_dm.AzureServicePrincipalCredentialProperties + // AzureWorkloadIdentityCredential represents a credential for Azure AD workload identity. + AzureWorkloadIdentityCredential = ucp_dm.AzureWorkloadIdentityCredentialProperties // AWSCredential represents a credential for AWS IAM. AWSCredential = ucp_dm.AWSCredentialProperties ) diff --git a/pkg/ucp/datamodel/credential.go b/pkg/ucp/datamodel/credential.go index 921308f1a3..9e0b426832 100644 --- a/pkg/ucp/datamodel/credential.go +++ b/pkg/ucp/datamodel/credential.go @@ -21,8 +21,10 @@ import v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" const ( // InternalStorageKind represents ucp credential storage type for internal credential type InternalStorageKind = "Internal" - // AzureCredentialKind represents ucp credential kind for azure credentials. - AzureCredentialKind = "ServicePrincipal" + // AzureServicePrincipalCredentialKind represents ucp credential kind for Azure service principal credentials. + AzureServicePrincipalCredentialKind = "ServicePrincipal" + // AzureWorkloadIdentityCredentialKind represents ucp credential kind for Azure workload identity credentials. + AzureWorkloadIdentityCredentialKind = "WorkloadIdentity" // AWSCredentialKind represents ucp credential kind for aws credentials. AWSCredentialKind = "AccessKey" ) @@ -53,9 +55,9 @@ func (c *AWSCredential) ResourceTypeName() string { // Azure Credential Properties represents UCP Credential Properties. type AzureCredentialResourceProperties struct { - // Kind is the kind of azure credential resource. + // Kind is the kind of Azure credential resource. Kind string `json:"kind,omitempty"` - // AzureCredential is the azure service principal credentials. + // AzureCredential is the Azure credentials. AzureCredential *AzureCredentialProperties `json:"azureCredential,omitempty"` // Storage contains the properties of the storage associated with the kind. Storage *CredentialStorageProperties `json:"storage,omitempty"` @@ -71,16 +73,33 @@ type AWSCredentialResourceProperties struct { Storage *CredentialStorageProperties `json:"storage,omitempty"` } -// AzureCredentialProperties contains ucp Azure credential properties. -type AzureCredentialProperties struct { - // TenantID represents the tenantId of azure service principal. +// AzureServicePrincipalCredentialProperties contains ucp Azure service principal credential properties. +type AzureServicePrincipalCredentialProperties struct { + // TenantID represents the tenantId of azure service principal credential. TenantID string `json:"tenantId"` - // ClientID represents the clientId of azure service principal. + // ClientID represents the clientId of azure service principal credential. ClientID string `json:"clientId"` - // ClientSecret represents the client secret of service principal. + // ClientSecret represents the client secret of service principal credential. ClientSecret string `json:"clientSecret,omitempty"` } +// AzureWorkloadIdentityCredentialProperties contains ucp Azure workload identity credential properties. +type AzureWorkloadIdentityCredentialProperties struct { + // TenantID represents the tenantId of azure workload identity credential. + TenantID string `json:"tenantId"` + // ClientID represents the clientId of azure service principal credential. + ClientID string `json:"clientId"` +} + +type AzureCredentialProperties struct { + // Kind is the kind of Azure credential. + Kind string `json:"kind,omitempty"` + // ServicePrincipal represents the service principal properties. + ServicePrincipal *AzureServicePrincipalCredentialProperties `json:"servicePrincipal,omitempty"` + // WorkloadIdentity represents the workload identity properties. + WorkloadIdentity *AzureWorkloadIdentityCredentialProperties `json:"workloadIdentity,omitempty"` +} + // AWSCredentialProperties contains ucp AWS credential properties. type AWSCredentialProperties struct { // AccessKeyID contains aws access key for iam. diff --git a/pkg/ucp/frontend/controller/credentials/azure/createorupdateazurecredential.go b/pkg/ucp/frontend/controller/credentials/azure/createorupdateazurecredential.go index 1149e5134d..40dd59fe36 100644 --- a/pkg/ucp/frontend/controller/credentials/azure/createorupdateazurecredential.go +++ b/pkg/ucp/frontend/controller/credentials/azure/createorupdateazurecredential.go @@ -61,7 +61,8 @@ func (c *CreateOrUpdateAzureCredential) Run(ctx context.Context, w http.Response return nil, err } - if newResource.Properties.Kind != datamodel.AzureCredentialKind { + if newResource.Properties.Kind != datamodel.AzureServicePrincipalCredentialKind && + newResource.Properties.Kind != datamodel.AzureWorkloadIdentityCredentialKind { return armrpc_rest.NewBadRequestResponse("Invalid Credential Kind"), nil } @@ -79,15 +80,30 @@ func (c *CreateOrUpdateAzureCredential) Run(ctx context.Context, w http.Response newResource.Properties.Storage.InternalCredential.SecretName = secretName } - // Save the credential secret - err = secret.SaveSecret(ctx, c.secretClient, secretName, newResource.Properties.AzureCredential) - if err != nil { - return nil, err + switch newResource.Properties.Kind { + case datamodel.AzureServicePrincipalCredentialKind: + if newResource.Properties.AzureCredential.ServicePrincipal == nil { + return armrpc_rest.NewBadRequestResponse("Invalid Service Principal Credential"), nil + } + // Save the credential secret + err = secret.SaveSecret(ctx, c.secretClient, secretName, newResource.Properties.AzureCredential) + if err != nil { + return nil, err + } + newResource.Properties.AzureCredential.ServicePrincipal.ClientSecret = "" + case datamodel.AzureWorkloadIdentityCredentialKind: + if newResource.Properties.AzureCredential.WorkloadIdentity == nil { + return armrpc_rest.NewBadRequestResponse("Invalid Workload Identity Credential"), nil + } + // Save the credential secret + err = secret.SaveSecret(ctx, c.secretClient, secretName, newResource.Properties.AzureCredential) + if err != nil { + return nil, err + } + default: + return armrpc_rest.NewBadRequestResponse("Invalid Credential Kind"), nil } - // Do not save the secret in metadata store. - newResource.Properties.AzureCredential.ClientSecret = "" - newResource.SetProvisioningState(v1.ProvisioningStateSucceeded) newEtag, err := c.SaveResource(ctx, serviceCtx.ResourceID.String(), newResource, etag) if err != nil { diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_CreateOrUpdate.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_CreateOrUpdate.json similarity index 95% rename from typespec/UCP/examples/2023-10-01-preview/AzureCredential_CreateOrUpdate.json rename to swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_CreateOrUpdate.json index 0742ef8e96..bd265833c8 100644 --- a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_CreateOrUpdate.json +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_CreateOrUpdate.json @@ -1,6 +1,6 @@ { "operationId": "AzureCredentials_CreateOrUpdate", - "title": "Create or update an Azure credential", + "title": "Create or update an Azure Service Principal credential", "parameters": { "api-version": "2023-10-01-preview", "planeType": "azure", diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_Delete.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Delete.json similarity index 80% rename from typespec/UCP/examples/2023-10-01-preview/AzureCredential_Delete.json rename to swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Delete.json index 26330012f6..1be9794e92 100644 --- a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_Delete.json +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Delete.json @@ -1,6 +1,6 @@ { "operationId": "AzureCredentials_Delete", - "title": "Delete an Azure credential", + "title": "Delete an Azure Service Principal credential", "parameters": { "api-version": "2023-10-01-preview", "planeType": "azure", diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_Get.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Get.json similarity index 93% rename from typespec/UCP/examples/2023-10-01-preview/AzureCredential_Get.json rename to swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Get.json index 23e6142ad6..630257a927 100644 --- a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_Get.json +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Get.json @@ -1,6 +1,6 @@ { "operationId": "AzureCredentials_Get", - "title": "Get an Azure credential", + "title": "Get an Azure Service Principal credential", "parameters": { "api-version": "2023-10-01-preview", "planeType": "azure", diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_List.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_List.json similarity index 93% rename from typespec/UCP/examples/2023-10-01-preview/AzureCredential_List.json rename to swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_List.json index 0238221ca5..037785e4b4 100644 --- a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_List.json +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_List.json @@ -1,6 +1,6 @@ { "operationId": "AzureCredentials_List", - "title": "List Azure credentials", + "title": "List Azure Service Principal credentials", "parameters": { "api-version": "2023-10-01-preview", "planeType": "azure", diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_Update.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Update.json similarity index 96% rename from typespec/UCP/examples/2023-10-01-preview/AzureCredential_Update.json rename to swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Update.json index 5db043a301..4a45fccfc1 100644 --- a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_Update.json +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_ServicePrincipal_Update.json @@ -1,6 +1,6 @@ { "operationId": "AzureCredentials_Update", - "title": "Update an Azure credential", + "title": "Update an Azure Service Principal credential", "parameters": { "api-version": "2023-10-01-preview", "planeType": "azure", diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_CreateOrUpdate.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_CreateOrUpdate.json new file mode 100644 index 0000000000..64a1cfe8e8 --- /dev/null +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_CreateOrUpdate.json @@ -0,0 +1,54 @@ +{ + "operationId": "AzureCredentials_CreateOrUpdate", + "title": "Create or update an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default", + "Credential": { + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "201": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Delete.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Delete.json new file mode 100644 index 0000000000..cd93a9d61a --- /dev/null +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Delete.json @@ -0,0 +1,14 @@ +{ + "operationId": "AzureCredentials_Delete", + "title": "Delete an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default" + }, + "responses": { + "200": {}, + "204": {} + } +} \ No newline at end of file diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Get.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Get.json new file mode 100644 index 0000000000..30d1059755 --- /dev/null +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Get.json @@ -0,0 +1,29 @@ +{ + "operationId": "AzureCredentials_Get", + "title": "Get an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default" + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_List.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_List.json new file mode 100644 index 0000000000..014d17965d --- /dev/null +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_List.json @@ -0,0 +1,32 @@ +{ + "operationId": "AzureCredentials_List", + "title": "List Azure Workload Identity credentials", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Update.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Update.json new file mode 100644 index 0000000000..3dc540a8f2 --- /dev/null +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/examples/AzureCredential_WorkloadIdentity_Update.json @@ -0,0 +1,56 @@ +{ + "operationId": "AzureCredentials_Update", + "title": "Update an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default", + "Credential": { + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "201": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/openapi.json b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/openapi.json index 3542f1fdad..7dca6e2eb9 100644 --- a/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/openapi.json +++ b/swagger/specification/ucp/resource-manager/UCP/preview/2023-10-01-preview/openapi.json @@ -856,8 +856,11 @@ } }, "x-ms-examples": { - "List Azure credentials": { - "$ref": "./examples/AzureCredential_List.json" + "List Azure Service Principal credentials": { + "$ref": "./examples/AzureCredential_ServicePrincipal_List.json" + }, + "List Azure Workload Identity credentials": { + "$ref": "./examples/AzureCredential_WorkloadIdentity_List.json" } }, "x-ms-pageable": { @@ -904,8 +907,11 @@ } }, "x-ms-examples": { - "Get an Azure credential": { - "$ref": "./examples/AzureCredential_Get.json" + "Get an Azure Service Principal credential": { + "$ref": "./examples/AzureCredential_ServicePrincipal_Get.json" + }, + "Get an Azure Workload Identity credential": { + "$ref": "./examples/AzureCredential_WorkloadIdentity_Get.json" } } }, @@ -962,8 +968,11 @@ } }, "x-ms-examples": { - "Create or update an Azure credential": { - "$ref": "./examples/AzureCredential_CreateOrUpdate.json" + "Create or update an Azure Service Principal credential": { + "$ref": "./examples/AzureCredential_ServicePrincipal_CreateOrUpdate.json" + }, + "Create or update an Azure Workload Identity credential": { + "$ref": "./examples/AzureCredential_WorkloadIdentity_CreateOrUpdate.json" } } }, @@ -1014,8 +1023,11 @@ } }, "x-ms-examples": { - "Update an Azure credential": { - "$ref": "./examples/AzureCredential_Update.json" + "Update an Azure Service Principal credential": { + "$ref": "./examples/AzureCredential_ServicePrincipal_Update.json" + }, + "Update an Azure Workload Identity credential": { + "$ref": "./examples/AzureCredential_WorkloadIdentity_Update.json" } } }, @@ -1057,8 +1069,11 @@ } }, "x-ms-examples": { - "Delete an Azure credential": { - "$ref": "./examples/AzureCredential_Delete.json" + "Delete an Azure Service Principal credential": { + "$ref": "./examples/AzureCredential_ServicePrincipal_Delete.json" + }, + "Delete an Azure Workload Identity credential": { + "$ref": "./examples/AzureCredential_WorkloadIdentity_Delete.json" } } } @@ -1829,7 +1844,8 @@ "type": "string", "description": "Azure credential kinds supported.", "enum": [ - "ServicePrincipal" + "ServicePrincipal", + "WorkloadIdentity" ], "x-ms-enum": { "name": "AzureCredentialKind", @@ -1839,6 +1855,11 @@ "name": "ServicePrincipal", "value": "ServicePrincipal", "description": "The Service Principal Credential" + }, + { + "name": "WorkloadIdentity", + "value": "WorkloadIdentity", + "description": "The Workload Identity Credential" } ] } @@ -1996,7 +2017,7 @@ }, "AzureServicePrincipalProperties": { "type": "object", - "description": "The properties of Service Principal credential storage", + "description": "The properties of Azure Service Principal credential storage", "properties": { "clientId": { "type": "string", @@ -2029,6 +2050,35 @@ ], "x-ms-discriminator-value": "ServicePrincipal" }, + "AzureWorkloadIdentityProperties": { + "type": "object", + "description": "The properties of Azure Workload Identity credential storage", + "properties": { + "clientId": { + "type": "string", + "description": "clientId for WorkloadIdentity" + }, + "tenantId": { + "type": "string", + "description": "tenantId for WorkloadIdentity" + }, + "storage": { + "$ref": "#/definitions/CredentialStorageProperties", + "description": "The storage properties" + } + }, + "required": [ + "clientId", + "tenantId", + "storage" + ], + "allOf": [ + { + "$ref": "#/definitions/AzureCredentialProperties" + } + ], + "x-ms-discriminator-value": "WorkloadIdentity" + }, "CredentialStorageKind": { "type": "string", "description": "Credential store kinds supported.", diff --git a/typespec/UCP/azure-credentials.tsp b/typespec/UCP/azure-credentials.tsp index 8739870271..ad18ba8150 100644 --- a/typespec/UCP/azure-credentials.tsp +++ b/typespec/UCP/azure-credentials.tsp @@ -64,6 +64,9 @@ model AzureCredentialResource enum AzureCredentialKind { @doc("The Service Principal Credential") ServicePrincipal, + + @doc("The Workload Identity Credential") + WorkloadIdentity, } @discriminator("kind") @@ -77,9 +80,9 @@ model AzureCredentialProperties { provisioningState?: ProvisioningState; } -@doc("The properties of Service Principal credential storage") +@doc("The properties of Azure Service Principal credential storage") model AzureServicePrincipalProperties extends AzureCredentialProperties { - @doc("Service principal kind") + @doc("Service Principal kind") kind: AzureCredentialKind.ServicePrincipal; @doc("clientId for ServicePrincipal") @@ -96,6 +99,21 @@ model AzureServicePrincipalProperties extends AzureCredentialProperties { storage: CredentialStorageProperties; } +@doc("The properties of Azure Workload Identity credential storage") +model AzureWorkloadIdentityProperties extends AzureCredentialProperties { + @doc("Workload Identity kind") + kind: AzureCredentialKind.WorkloadIdentity; + + @doc("clientId for WorkloadIdentity") + clientId: string; + + @doc("tenantId for WorkloadIdentity") + tenantId: string; + + @doc("The storage properties") + storage: CredentialStorageProperties; +} + alias AzureCredentialBaseParameter = CredentialBaseParameters< TResource, AzurePlaneNameParameter diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_CreateOrUpdate.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_CreateOrUpdate.json new file mode 100644 index 0000000000..bd265833c8 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_CreateOrUpdate.json @@ -0,0 +1,57 @@ +{ + "operationId": "AzureCredentials_CreateOrUpdate", + "title": "Create or update an Azure Service Principal credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default", + "Credential": { + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "clientId": "00000000-0000-0000-0000-000000000000", + "clientSecret": "secretString", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "201": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Delete.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Delete.json new file mode 100644 index 0000000000..1be9794e92 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Delete.json @@ -0,0 +1,14 @@ +{ + "operationId": "AzureCredentials_Delete", + "title": "Delete an Azure Service Principal credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default" + }, + "responses": { + "200": {}, + "204": {} + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Get.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Get.json new file mode 100644 index 0000000000..630257a927 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Get.json @@ -0,0 +1,29 @@ +{ + "operationId": "AzureCredentials_Get", + "title": "Get an Azure Service Principal credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default" + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_List.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_List.json new file mode 100644 index 0000000000..037785e4b4 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_List.json @@ -0,0 +1,32 @@ +{ + "operationId": "AzureCredentials_List", + "title": "List Azure Service Principal credentials", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Update.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Update.json new file mode 100644 index 0000000000..4a45fccfc1 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_ServicePrincipal_Update.json @@ -0,0 +1,57 @@ +{ + "operationId": "AzureCredentials_Update", + "title": "Update an Azure Service Principal credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default", + "Credential": { + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "clientId": "00000000-0000-0000-0000-000000000000", + "clientSecret": "secretString", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "201": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "ServicePrincipal", + "tenantId": "00000000-0000-0000-0000-000000000000", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_CreateOrUpdate.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_CreateOrUpdate.json new file mode 100644 index 0000000000..64a1cfe8e8 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_CreateOrUpdate.json @@ -0,0 +1,54 @@ +{ + "operationId": "AzureCredentials_CreateOrUpdate", + "title": "Create or update an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default", + "Credential": { + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "201": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Delete.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Delete.json new file mode 100644 index 0000000000..cd93a9d61a --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Delete.json @@ -0,0 +1,14 @@ +{ + "operationId": "AzureCredentials_Delete", + "title": "Delete an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default" + }, + "responses": { + "200": {}, + "204": {} + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Get.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Get.json new file mode 100644 index 0000000000..30d1059755 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Get.json @@ -0,0 +1,29 @@ +{ + "operationId": "AzureCredentials_Get", + "title": "Get an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default" + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_List.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_List.json new file mode 100644 index 0000000000..014d17965d --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_List.json @@ -0,0 +1,32 @@ +{ + "operationId": "AzureCredentials_List", + "title": "List Azure Workload Identity credentials", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Update.json b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Update.json new file mode 100644 index 0000000000..3dc540a8f2 --- /dev/null +++ b/typespec/UCP/examples/2023-10-01-preview/AzureCredential_WorkloadIdentity_Update.json @@ -0,0 +1,56 @@ +{ + "operationId": "AzureCredentials_Update", + "title": "Update an Azure Workload Identity credential", + "parameters": { + "api-version": "2023-10-01-preview", + "planeType": "azure", + "planeName": "azurecloud", + "credentialName": "default", + "Credential": { + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "responses": { + "200": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal" + } + } + } + }, + "201": { + "body": { + "id": "/planes/azure/azurecloud/providers/System.Azure/credentials/default", + "name": "default", + "type": "System.Azure/credentials", + "location": "west-us-2", + "properties": { + "kind": "WorkloadIdentity", + "clientId": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000000", + "storage": { + "kind": "Internal", + "secretName": "azure-azurecloud-default" + } + } + } + } + } +} \ No newline at end of file