From b34fdd58c3ce6d4fa95552c880cbcfb5c1cb3366 Mon Sep 17 00:00:00 2001 From: Jonasz Lasut-Balcerzak Date: Mon, 25 Aug 2025 15:49:53 +0200 Subject: [PATCH 1/4] feat: add workload identity authentication support Add support for Azure Workload Identity authentication alongside existing service principal authentication for Microsoft Graph API access. --- README.md | 85 ++++++++++++++++- fn.go | 93 ++++++++++++++++--- input/v1beta1/input.go | 20 ++++ input/v1beta1/zz_generated.deepcopy.go | 20 ++++ .../msgraph.fn.crossplane.io_inputs.yaml | 13 ++- 5 files changed, 213 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 17738c9..e04d0fe 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ spec: ### Azure Credentials +The service principal needs the following Microsoft Graph API permissions: +- User.Read.All (for user validation) +- Group.Read.All (for group operations) +- Application.Read.All (for service principal details) + +#### Client Secret Credentials Create an Azure service principal with appropriate permissions to access Microsoft Graph API: ```yaml @@ -47,10 +53,81 @@ stringData: } ``` -The service principal needs the following Microsoft Graph API permissions: -- User.Read.All (for user validation) -- Group.Read.All (for group operations) -- Application.Read.All (for service principal details) +#### Workload Identity Credentials + +The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html + +Credentials secret: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: azure-account-creds + namespace: crossplane-system +type: Opaque +stringData: + credentials: | + { + "clientId": "your-client-id", + "tenantId": "your-tenant-id", + "federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token" + } +``` + +Function +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: upbound-function-msgraph +spec: + package: xpkg.upbound.io/upbound/function-msgraph:v0.2.0 + runtimeConfigRef: + apiVersion: pkg.crossplane.io/v1beta1 + kind: DeploymentRuntimeConfig + name: upbound-function-msgraph +``` + +DeploymentRuntimeConfig +```yaml +apiVersion: pkg.crossplane.io/v1beta1 +kind: DeploymentRuntimeConfig +metadata: + name: upbound-function-msgraph +spec: + deploymentTemplate: + spec: + selector: + matchLabels: + azure.workload.identity/use: "true" + pkg.crossplane.io/function: "upbound-function-msgraph" + template: + metadata: + labels: + azure.workload.identity/use: "true" + pkg.crossplane.io/function: "upbound-function-msgraph" + spec: + containers: + - name: package-runtime + volumeMounts: + - mountPath: /var/run/secrets/azure/tokens + name: azure-identity-token + readOnly: true + serviceAccountName: "upbound-function-msgraph" + volumes: + - name: azure-identity-token + projected: + sources: + - serviceAccountToken: + audience: api://AzureADTokenExchange + expirationSeconds: 3600 + path: azure-identity-token + serviceAccountTemplate: + metadata: + annotations: + azure.workload.identity/client-id: "your-client-id" + name: "upbound-function-msgraph" +``` ## Examples diff --git a/fn.go b/fn.go index e584700..bfd8f7b 100644 --- a/fn.go +++ b/fn.go @@ -26,6 +26,22 @@ import ( "github.com/crossplane/function-sdk-go/response" ) +var ( + // MSGraphScopes defines the default MS Graph scopes + MSGraphScopes = []string{"https://graph.microsoft.com/.default"} +) + +const ( + // TenantID defines the azure credentials key for tenant id + TenantID = "tenantId" + // ClientID defines the azure credentials key for client id + ClientID = "clientId" + // ClientSecret defines the azure credentials key for client secret + ClientSecret = "clientSecret" + // WorkloadIdentityCredentialPath defines the azure credentials key for federated token file path + WorkloadIdentityCredentialPath = "federatedTokenFile" +) + // GraphQueryInterface defines the methods required for querying Microsoft Graph API. type GraphQueryInterface interface { graphQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (interface{}, error) @@ -264,37 +280,88 @@ type GraphQuery struct { } // createGraphClient initializes a Microsoft Graph client using the provided credentials -func (g *GraphQuery) createGraphClient(azureCreds map[string]string) (*msgraphsdk.GraphServiceClient, error) { - tenantID := azureCreds["tenantId"] - clientID := azureCreds["clientId"] - clientSecret := azureCreds["clientSecret"] +func (g *GraphQuery) createGraphClient(azureCreds map[string]string, identityType v1beta1.IdentityType) (client *msgraphsdk.GraphServiceClient, err error) { + authProvider := &azauth.AzureIdentityAuthenticationProvider{} + + switch identityType { + case v1beta1.IdentityTypeAzureWorkloadIdentityCredentials: + authProvider, err = g.initializeWorkloadIdentityProvider(azureCreds) + if err != nil { + return nil, errors.Wrap(err, "failed to create auth provider") + } + case v1beta1.IdentityTypeAzureServicePrincipalCredentials: + authProvider, err = g.initializeClientSecretProvider(azureCreds) + if err != nil { + return nil, errors.Wrap(err, "failed to create auth provider") + } + } + + // Create adapter + adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider) + if err != nil { + return nil, errors.Wrap(err, "failed to create graph adapter") + } + + // Initialize Microsoft Graph client + return msgraphsdk.NewGraphServiceClient(adapter), nil +} + +func (g *GraphQuery) initializeClientSecretProvider(azureCreds map[string]string) (*azauth.AzureIdentityAuthenticationProvider, error) { + tenantID := azureCreds[TenantID] + clientID := azureCreds[ClientID] + clientSecret := azureCreds[ClientSecret] // Create Azure credential for Microsoft Graph cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) if err != nil { - return nil, errors.Wrap(err, "failed to obtain credentials") + return nil, errors.Wrap(err, "failed to obtain clientsecret credentials") } - // Create authentication provider - authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{"https://graph.microsoft.com/.default"}) + authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, MSGraphScopes) if err != nil { return nil, errors.Wrap(err, "failed to create auth provider") } - // Create adapter - adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider) + return authProvider, nil +} + +func (g *GraphQuery) initializeWorkloadIdentityProvider(azureCreds map[string]string) (*azauth.AzureIdentityAuthenticationProvider, error) { + options := &azidentity.WorkloadIdentityCredentialOptions{ + TokenFilePath: azureCreds[WorkloadIdentityCredentialPath], + } + + tenantID, found := azureCreds[TenantID] + if found { + options.TenantID = tenantID + } + clientID, found := azureCreds[ClientID] + if found { + options.ClientID = clientID + } + + // Create Azure credential for Microsoft Graph + cred, err := azidentity.NewWorkloadIdentityCredential(options) if err != nil { - return nil, errors.Wrap(err, "failed to create graph adapter") + return nil, errors.Wrap(err, "failed to obtain workloadidentity credentials") + } + // Create authentication provider + authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, MSGraphScopes) + if err != nil { + return nil, errors.Wrap(err, "failed to create auth provider") } - // Initialize Microsoft Graph client - return msgraphsdk.NewGraphServiceClient(adapter), nil + return authProvider, nil } // graphQuery is a concrete implementation that interacts with Microsoft Graph API. func (g *GraphQuery) graphQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (interface{}, error) { + identityType := v1beta1.IdentityTypeAzureServicePrincipalCredentials + + if in.Identity != nil { + identityType = in.Identity.Type + } // Create the Microsoft Graph client - client, err := g.createGraphClient(azureCreds) + client, err := g.createGraphClient(azureCreds, identityType) if err != nil { return nil, err } diff --git a/input/v1beta1/input.go b/input/v1beta1/input.go index 3c3b5b1..41ccefc 100644 --- a/input/v1beta1/input.go +++ b/input/v1beta1/input.go @@ -66,4 +66,24 @@ type Input struct { // Default is false to ensure continuous reconciliation // +optional SkipQueryWhenTargetHasData *bool `json:"skipQueryWhenTargetHasData,omitempty"` + + // Identity defines the type of identity used for authentication to the Microsoft Graph API. + Identity *Identity `json:"identity,omitempty"` +} + +// Identity defines the type of identity used for authentication to the Microsoft Graph API. +type Identity struct { + // Type of credentials used to authenticate to the Microsoft Graph API. + Type IdentityType `json:"type"` } + +const ( + // IdentityTypeAzureServicePrincipalCredentials defines default IdentityType which uses client id/client secret pair for authentication + IdentityTypeAzureServicePrincipalCredentials IdentityType = "AzureServicePrincipalCredentials" + // IdentityTypeAzureWorkloadIdentityCredentials defines default IdentityType which uses workload identity credentials for authentication + IdentityTypeAzureWorkloadIdentityCredentials IdentityType = "AzureWorkloadIdentityCredentials" +) + +// IdentityType controls type of credentials to use for authentication to the Microsoft Graph API. +// Supported values: AzureServicePrincipalCredentials;AzureWorkloadIdentityCredentials +type IdentityType string diff --git a/input/v1beta1/zz_generated.deepcopy.go b/input/v1beta1/zz_generated.deepcopy.go index a12618f..7d40fb2 100644 --- a/input/v1beta1/zz_generated.deepcopy.go +++ b/input/v1beta1/zz_generated.deepcopy.go @@ -8,6 +8,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Identity) DeepCopyInto(out *Identity) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Identity. +func (in *Identity) DeepCopy() *Identity { + if in == nil { + return nil + } + out := new(Identity) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Input) DeepCopyInto(out *Input) { *out = *in @@ -76,6 +91,11 @@ func (in *Input) DeepCopyInto(out *Input) { *out = new(bool) **out = **in } + if in.Identity != nil { + in, out := &in.Identity, &out.Identity + *out = new(Identity) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Input. diff --git a/package/input/msgraph.fn.crossplane.io_inputs.yaml b/package/input/msgraph.fn.crossplane.io_inputs.yaml index 02cb48f..cfeb7f8 100644 --- a/package/input/msgraph.fn.crossplane.io_inputs.yaml +++ b/package/input/msgraph.fn.crossplane.io_inputs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.3 + controller-gen.kubebuilder.io/version: v0.18.0 name: inputs.msgraph.fn.crossplane.io spec: group: msgraph.fn.crossplane.io @@ -46,6 +46,17 @@ spec: GroupsRef is a reference to retrieve the group names (e.g., from status or context) Overrides Groups field if used type: string + identity: + description: Identity defines the type of identity used for authentication + to the Microsoft Graph API. + properties: + type: + description: Type of credentials used to authenticate to the Microsoft + Graph API. + type: string + required: + - type + type: object kind: description: |- Kind is a string value representing the REST resource this object represents. From e9d54299814d8dbd347df3e9148e3fdbf4e24223 Mon Sep 17 00:00:00 2001 From: Jonasz Lasut-Balcerzak Date: Tue, 26 Aug 2025 13:30:32 +0200 Subject: [PATCH 2/4] comments and empty string check --- fn.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fn.go b/fn.go index bfd8f7b..2740803 100644 --- a/fn.go +++ b/fn.go @@ -330,10 +330,13 @@ func (g *GraphQuery) initializeWorkloadIdentityProvider(azureCreds map[string]st TokenFilePath: azureCreds[WorkloadIdentityCredentialPath], } + // Defaults to the value of the environment variable AZURE_TENANT_ID tenantID, found := azureCreds[TenantID] if found { options.TenantID = tenantID } + + // Defaults to the value of the environment variable AZURE_CLIENT_ID clientID, found := azureCreds[ClientID] if found { options.ClientID = clientID @@ -357,7 +360,7 @@ func (g *GraphQuery) initializeWorkloadIdentityProvider(azureCreds map[string]st func (g *GraphQuery) graphQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (interface{}, error) { identityType := v1beta1.IdentityTypeAzureServicePrincipalCredentials - if in.Identity != nil { + if in.Identity != nil && in.Identity.Type != "" { identityType = in.Identity.Type } // Create the Microsoft Graph client From 574cb0f16207faee8932d449dd76b5510fd9fc65 Mon Sep 17 00:00:00 2001 From: Jonasz Lasut-Balcerzak Date: Tue, 26 Aug 2025 15:00:38 +0200 Subject: [PATCH 3/4] test credential type choice --- README.md | 41 +++- example/identity-type-workload-identity.yaml | 35 +++ .../azure-workload-identity-creds.yaml | 11 + fn.go | 5 +- fn_test.go | 216 ++++++++++++++++++ 5 files changed, 299 insertions(+), 9 deletions(-) create mode 100644 example/identity-type-workload-identity.yaml create mode 100644 example/secrets/azure-workload-identity-creds.yaml diff --git a/README.md b/README.md index e04d0fe..a8b8112 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,10 @@ stringData: ``` #### Workload Identity Credentials +AKS cluster needs to have workload identity enabled. +The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html. -The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html - -Credentials secret: +##### Credentials secret: ```yaml apiVersion: v1 kind: Secret @@ -68,13 +68,13 @@ type: Opaque stringData: credentials: | { - "clientId": "your-client-id", - "tenantId": "your-tenant-id", + "clientId": "your-client-id", # optional + "tenantId": "your-tenant-id", # optional "federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token" } ``` -Function +##### Function ```yaml apiVersion: pkg.crossplane.io/v1 kind: Function @@ -88,7 +88,7 @@ spec: name: upbound-function-msgraph ``` -DeploymentRuntimeConfig +##### DeploymentRuntimeConfig ```yaml apiVersion: pkg.crossplane.io/v1beta1 kind: DeploymentRuntimeConfig @@ -275,6 +275,7 @@ spec: | `servicePrincipalsRef` | string | Reference to resolve a list of service principal names from `spec`, `status` or `context` (e.g., `spec.servicePrincipalConfig.names`) | | `target` | string | Required. Where to store the query results. Can be `status.` or `context.` | | `skipQueryWhenTargetHasData` | bool | Optional. When true, will skip the query if the target already has data | +| `identity.type | string | Optional. Type of identity credentials to use. Valid values: `AzureServicePrincipalCredentials`, `AzureWorkloadIdentityCredentials`. Default is `AzureServicePrincipalCredentials` | ## Result Targets @@ -338,6 +339,32 @@ servicePrincipalsRef: "spec.servicePrincipalConfig.names" # Get service princip target: "status.servicePrincipals" ``` +## Using Different Credentials + +### Using ServicePrincipal credentials + +#### Explicitly +```yaml +apiVersion: msgraph.fn.crossplane.io/v1alpha1 +kind: Input +identity: + type: AzureServicePrincipalCredentials +``` + +#### Default +```yaml +apiVersion: msgraph.fn.crossplane.io/v1alpha1 +kind: Input +``` + +### Using Workload Identity Credentials +```yaml +apiVersion: msgraph.fn.crossplane.io/v1alpha1 +kind: Input +identity: + type: AzureWorkloadIdentityCredentials +``` + ## References - [Microsoft Graph API Overview](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) diff --git a/example/identity-type-workload-identity.yaml b/example/identity-type-workload-identity.yaml new file mode 100644 index 0000000..4553adf --- /dev/null +++ b/example/identity-type-workload-identity.yaml @@ -0,0 +1,35 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: workload-identity-example +# Important: This function example requires an Azure AD app registration with Microsoft Graph API permissions: +# - User.Read.All +# - Directory.Read.All +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: XR + mode: Pipeline + pipeline: + - step: validate-user + functionRef: + name: function-msgraph + input: + apiVersion: msgraph.fn.crossplane.io/v1alpha1 + kind: Input + queryType: UserValidation + # Replace these with actual users in your directory + users: + - "admin@example.onmicrosoft.com" + - "user@example.onmicrosoft.com" + - "yury@upbound.io" + target: "status.validatedUsers" + skipQueryWhenTargetHasData: true + identity: + type: AzureWorkloadIdentityCredentials + credentials: + - name: azure-creds + source: Secret + secretRef: + namespace: upbound-system + name: azure-workload-identity-creds diff --git a/example/secrets/azure-workload-identity-creds.yaml b/example/secrets/azure-workload-identity-creds.yaml new file mode 100644 index 0000000..84129d1 --- /dev/null +++ b/example/secrets/azure-workload-identity-creds.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +stringData: + credentials: | + { + "federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token" + } +kind: Secret +metadata: + name: azure-workload-identity-creds + namespace: upbound-system +type: Opaque diff --git a/fn.go b/fn.go index 2740803..d320ea5 100644 --- a/fn.go +++ b/fn.go @@ -73,6 +73,7 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil //nolint:nilerr // errors are handled in rsp } + fmt.Println("We're here") // Validate and prepare input if !f.validateAndPrepareInput(ctx, req, in, rsp) { return rsp, nil // Early return if validation failed or query should be skipped @@ -287,12 +288,12 @@ func (g *GraphQuery) createGraphClient(azureCreds map[string]string, identityTyp case v1beta1.IdentityTypeAzureWorkloadIdentityCredentials: authProvider, err = g.initializeWorkloadIdentityProvider(azureCreds) if err != nil { - return nil, errors.Wrap(err, "failed to create auth provider") + return nil, errors.Wrap(err, "failed to initialize workload identity provider") } case v1beta1.IdentityTypeAzureServicePrincipalCredentials: authProvider, err = g.initializeClientSecretProvider(azureCreds) if err != nil { - return nil, errors.Wrap(err, "failed to create auth provider") + return nil, errors.Wrap(err, "failed to initialize service principal provider") } } diff --git a/fn_test.go b/fn_test.go index e8eb210..b61010b 100644 --- a/fn_test.go +++ b/fn_test.go @@ -2505,3 +2505,219 @@ func TestRunFunction(t *testing.T) { }) } } + +func TestIdentityType(t *testing.T) { + var ( + xr = `{ + "apiVersion": "example.org/v1", + "kind": "XR", + "status": { + "groups": ["Developers", "Operations", "All Company"] + }}` + servicePrincipalCreds = &fnv1.CredentialData{ + Data: map[string][]byte{ + "credentials": []byte(`{ +"clientId": "test-client-id", +"clientSecret": "test-client-secret", +"subscriptionId": "test-subscription-id", +"tenantId": "test-tenant-id" +}`), + }, + } + workloadIdentityCredentials = &fnv1.CredentialData{ + Data: map[string][]byte{ + "credentials": []byte(`{ +"federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token" +}`), + }, + } + ) + + type args struct { + ctx context.Context + req *fnv1.RunFunctionRequest + } + type want struct { + rsp *fnv1.RunFunctionResponse + err error + } + + cases := map[string]struct { + reason string + args args + want want + }{ + "AzureServicePrincipalCredentialsImplicit": { + reason: "The Function should default to identity.type AzureServicePrincipalCredentials", + args: args{ + ctx: context.Background(), + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "hello"}, + Input: resource.MustStructJSON(`{ + "apiVersion": "msgraph.fn.crossplane.io/v1alpha1", + "kind": "Input", + "queryType": "GroupObjectIDs", + "groupsRef": "status.groups", + "target": "status.groupObjectIDs" + }`), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Credentials: map[string]*fnv1.Credentials{ + "azure-creds": { + Source: &fnv1.Credentials_CredentialData{CredentialData: servicePrincipalCreds}, + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{ + { + Severity: fnv1.Severity_SEVERITY_FATAL, + Message: `failed to initialize service principal provider: failed to obtain clientsecret credentials`, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "AzureServicePrincipalCredentialsExplicit": { + reason: "The Function should use ServicePrincipal credentials if identity.type is AzureServicePrincipalCredentials", + args: args{ + ctx: context.Background(), + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "hello"}, + Input: resource.MustStructJSON(`{ + "apiVersion": "msgraph.fn.crossplane.io/v1alpha1", + "kind": "Input", + "queryType": "GroupObjectIDs", + "groupsRef": "status.groups", + "target": "status.groupObjectIDs", + "identity": { + "type": "AzureServicePrincipalCredentials" + } + }`), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Credentials: map[string]*fnv1.Credentials{ + "azure-creds": { + Source: &fnv1.Credentials_CredentialData{CredentialData: servicePrincipalCreds}, + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{ + { + Severity: fnv1.Severity_SEVERITY_FATAL, + Message: `failed to initialize service principal provider: failed to obtain clientsecret credentials`, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "AzureWorkloadIdentityCredentials": { + reason: "The Function should use Workload Identity credentials if identity.type is AzureWorkloadIdentityCredentials", + args: args{ + ctx: context.Background(), + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "hello"}, + Input: resource.MustStructJSON(`{ + "apiVersion": "msgraph.fn.crossplane.io/v1alpha1", + "kind": "Input", + "queryType": "GroupObjectIDs", + "groupsRef": "status.groups", + "target": "status.groupObjectIDs", + "identity": { + "type": "AzureWorkloadIdentityCredentials" + } + }`), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Credentials: map[string]*fnv1.Credentials{ + "azure-creds": { + Source: &fnv1.Credentials_CredentialData{CredentialData: workloadIdentityCredentials}, + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{ + { + Severity: fnv1.Severity_SEVERITY_FATAL, + Message: `failed to initialize workload identity provider: failed to obtain workloadidentity credentials`, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // Create mock responders for each type of query + mockQuery := &MockGraphQuery{ + GraphQueryFunc: func(_ context.Context, _ map[string]string, in *v1beta1.Input) (interface{}, error) { + identityType := v1beta1.IdentityTypeAzureServicePrincipalCredentials + + if in.Identity != nil && in.Identity.Type != "" { + identityType = in.Identity.Type + } + + switch identityType { + case v1beta1.IdentityTypeAzureWorkloadIdentityCredentials: + return nil, errors.New("failed to initialize workload identity provider: failed to obtain workloadidentity credentials") + case v1beta1.IdentityTypeAzureServicePrincipalCredentials: + return nil, errors.New("failed to initialize service principal provider: failed to obtain clientsecret credentials") + default: + return nil, errors.Errorf("unsupported identity.type: %s", string(identityType)) + } + }, + } + + f := &Function{ + graphQuery: mockQuery, + log: logging.NewNopLogger(), + } + rsp, err := f.RunFunction(tc.args.ctx, tc.args.req) + + if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" { + t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff) + } + + if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff) + } + }) + } +} From c46a1200c9aab0ba5a3a9c437ca3ac73812c8f99 Mon Sep 17 00:00:00 2001 From: Jonasz Lasut-Balcerzak Date: Tue, 26 Aug 2025 15:06:37 +0200 Subject: [PATCH 4/4] remove debug println --- fn.go | 1 - 1 file changed, 1 deletion(-) diff --git a/fn.go b/fn.go index d320ea5..ec8f8b5 100644 --- a/fn.go +++ b/fn.go @@ -73,7 +73,6 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil //nolint:nilerr // errors are handled in rsp } - fmt.Println("We're here") // Validate and prepare input if !f.validateAndPrepareInput(ctx, req, in, rsp) { return rsp, nil // Early return if validation failed or query should be skipped