From c389268db7172cdebf7a8af80df47e99706e3f26 Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Mon, 6 Jan 2025 13:10:35 +0100 Subject: [PATCH 1/7] Mock out azure credentials Signed-off-by: Yury Tsarev --- fn.go | 2 +- fn_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/fn.go b/fn.go index a423ff7..3d30786 100644 --- a/fn.go +++ b/fn.go @@ -81,7 +81,7 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) if err != nil { - response.Fatal(rsp, errors.Wrap(err, "failed to obtain a credentials")) + response.Fatal(rsp, errors.Wrap(err, "failed to obtain credentials")) return rsp, nil } diff --git a/fn_test.go b/fn_test.go index cdfa0de..958b379 100644 --- a/fn_test.go +++ b/fn_test.go @@ -17,6 +17,19 @@ import ( func TestRunFunction(t *testing.T) { + var ( + xr = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2}}` + creds = &fnv1.CredentialData{ + Data: map[string][]byte{ + "credentials": []byte(`{ +"clientId": "test-cliend-id", +"clientSecret": "test-client-secret", +"subscriptionId": "test-subscription-id", +"tenantId": "test-tenant-id" +}`), + }, + } + ) type args struct { ctx context.Context req *fnv1.RunFunctionRequest @@ -82,6 +95,43 @@ func TestRunFunction(t *testing.T) { }, }, }, + "ShouldUpdateXRStatus": { + reason: "The Function should update XR status", + args: args{ + ctx: context.Background(), + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: "hello"}, + Input: resource.MustStructJSON(`{ + "apiVersion": "azresourcegraph.fn.crossplane.io/v1alpha1", + "kind": "Input", + "query": "Resources| count", + "managementGroups": ["test"] + }`), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Credentials: map[string]*fnv1.Credentials{ + "azure-creds": { + Source: &fnv1.Credentials_CredentialData{CredentialData: creds}, + }, + }, + }, + }, + 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 get azure-creds credentials", + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + }, + }, + }, } for name, tc := range cases { From 87a5b2e40a43d074c3e605ed88c1d868287281f1 Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Mon, 6 Jan 2025 13:22:52 +0100 Subject: [PATCH 2/7] Refactor credentials getting to separate function Signed-off-by: Yury Tsarev --- fn.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/fn.go b/fn.go index 3d30786..8b1d1dc 100644 --- a/fn.go +++ b/fn.go @@ -54,20 +54,9 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil } - var azureCreds map[string]string - rawCreds := req.GetCredentials() - - if credsData, ok := rawCreds["azure-creds"]; ok { - credsData := credsData.GetCredentialData().GetData() - if credsJSON, ok := credsData["credentials"]; ok { - err := json.Unmarshal(credsJSON, &azureCreds) - if err != nil { - response.Fatal(rsp, errors.Wrap(err, "cannot parse json credentials")) - return rsp, nil - } - } - } else { - response.Fatal(rsp, errors.New("failed to get azure-creds credentials")) + azureCreds, err := getCreds(req) + if err != nil { + response.Fatal(rsp, err) return rsp, nil } @@ -163,3 +152,22 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil } + +func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { + var azureCreds map[string]string + rawCreds := req.GetCredentials() + + if credsData, ok := rawCreds["azure-creds"]; ok { + credsData := credsData.GetCredentialData().GetData() + if credsJSON, ok := credsData["credentials"]; ok { + err := json.Unmarshal(credsJSON, &azureCreds) + if err != nil { + return nil, errors.Wrap(err, "cannot parse json credentials") + } + } + } else { + return nil, errors.New("failed to get azure-creds credentials") + } + + return azureCreds, nil +} From 65b9ed93dbc55e87c35bb805ddd1fa0404ac2cf8 Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Mon, 6 Jan 2025 14:00:12 +0100 Subject: [PATCH 3/7] Refactor query logic to separate function Signed-off-by: Yury Tsarev --- fn.go | 78 ++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/fn.go b/fn.go index 8b1d1dc..a6499f1 100644 --- a/fn.go +++ b/fn.go @@ -60,43 +60,9 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil } - tenantID := azureCreds["tenantId"] - clientID := azureCreds["clientId"] - clientSecret := azureCreds["clientSecret"] - subscriptionID := azureCreds["subscriptionId"] - - // To configure DefaultAzureCredential to authenticate a user-assigned managed identity, - // set the environment variable AZURE_CLIENT_ID to the identity's client ID. - - cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) + results, err := azQuery(ctx, req, azureCreds, in) if err != nil { - response.Fatal(rsp, errors.Wrap(err, "failed to obtain credentials")) - return rsp, nil - } - - // Create and authorize a ResourceGraph client - client, err := armresourcegraph.NewClient(cred, nil) - if err != nil { - response.Fatal(rsp, errors.Wrap(err, "failed to create client")) - return rsp, nil - } - - queryRequest := armresourcegraph.QueryRequest{ - Query: to.Ptr(in.Query), - } - - if len(subscriptionID) > 0 { - queryRequest.Subscriptions = []*string{to.Ptr(subscriptionID)} - } - - if len(in.ManagementGroups) > 0 { - queryRequest.ManagementGroups = in.ManagementGroups - } - - // Create the query request, Run the query and get the results. - results, err := client.Resources(ctx, queryRequest, nil) - if err != nil { - response.Fatal(rsp, errors.Wrap(err, "failed to finish the request")) + response.Fatal(rsp, err) f.log.Info("FAILURE: ", "failure", fmt.Sprint(err)) return rsp, nil } @@ -171,3 +137,43 @@ func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { return azureCreds, nil } + +func azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { + tenantID := azureCreds["tenantId"] + clientID := azureCreds["clientId"] + clientSecret := azureCreds["clientSecret"] + subscriptionID := azureCreds["subscriptionId"] + + // To configure DefaultAzureCredential to authenticate a user-assigned managed identity, + // set the environment variable AZURE_CLIENT_ID to the identity's client ID. + + cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) + if err != nil { + return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to obtain credentials") + } + + // Create and authorize a ResourceGraph client + client, err := armresourcegraph.NewClient(cred, nil) + if err != nil { + return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to create client") + } + + queryRequest := armresourcegraph.QueryRequest{ + Query: to.Ptr(in.Query), + } + + if len(subscriptionID) > 0 { + queryRequest.Subscriptions = []*string{to.Ptr(subscriptionID)} + } + + if len(in.ManagementGroups) > 0 { + queryRequest.ManagementGroups = in.ManagementGroups + } + + // Create the query request, Run the query and get the results. + results, err := client.Resources(ctx, queryRequest, nil) + if err != nil { + return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to finish the request") + } + return results, nil +} From b1d050b1b642381f25276f786e124c754f5b3894 Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Mon, 6 Jan 2025 14:49:39 +0100 Subject: [PATCH 4/7] Mock out azquery and test query result to XR status field Signed-off-by: Yury Tsarev --- fn.go | 10 ++++++++-- fn_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/fn.go b/fn.go index a6499f1..98fb327 100644 --- a/fn.go +++ b/fn.go @@ -20,10 +20,16 @@ import ( // TargetXRStatusField is the target field to write the query result to const TargetXRStatusField = "status.azResourceGraphQueryResult" +type AzureQueryInterface interface { + azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) +} + // Function returns whatever response you ask it to. type Function struct { fnv1.UnimplementedFunctionRunnerServiceServer + azureQuery AzureQueryInterface + log logging.Logger } @@ -60,7 +66,7 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil } - results, err := azQuery(ctx, req, azureCreds, in) + results, err := f.azureQuery.azQuery(ctx, req, azureCreds, in) if err != nil { response.Fatal(rsp, err) f.log.Info("FAILURE: ", "failure", fmt.Sprint(err)) @@ -138,7 +144,7 @@ func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { return azureCreds, nil } -func azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { +func (f *Function) azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { tenantID := azureCreds["tenantId"] clientID := azureCreds["clientId"] clientSecret := azureCreds["clientSecret"] diff --git a/fn_test.go b/fn_test.go index 958b379..91d6eea 100644 --- a/fn_test.go +++ b/fn_test.go @@ -4,8 +4,11 @@ import ( "context" "testing" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/upboundcare/function-azresourcegraph/input/v1beta1" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/durationpb" @@ -15,6 +18,14 @@ import ( "github.com/crossplane/function-sdk-go/response" ) +type MockAzureQuery struct { + AzQueryFunc func(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) +} + +func (m *MockAzureQuery) azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { + return m.AzQueryFunc(ctx, req, azureCreds, in) +} + func TestRunFunction(t *testing.T) { var ( @@ -30,6 +41,7 @@ func TestRunFunction(t *testing.T) { }, } ) + type args struct { ctx context.Context req *fnv1.RunFunctionRequest @@ -122,13 +134,35 @@ func TestRunFunction(t *testing.T) { want: want{ rsp: &fnv1.RunFunctionResponse{ Meta: &fnv1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, + Conditions: []*fnv1.Condition{ + { + Type: "FunctionSuccess", + Status: fnv1.Status_STATUS_CONDITION_TRUE, + Reason: "Success", + Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(), + }, + }, Results: []*fnv1.Result{ { - Severity: fnv1.Severity_SEVERITY_FATAL, - Message: "failed to get azure-creds credentials", + Severity: fnv1.Severity_SEVERITY_NORMAL, + Message: `Query: "Resources| count"`, Target: fnv1.Target_TARGET_COMPOSITE.Enum(), }, }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{ +"apiVersion": "example.org/v1", +"kind": "XR", +"status": { + "azResourceGraphQueryResult": [ + { + "resource": "mock-resource" + } + ] +}}`), + }, + }, }, }, }, @@ -136,7 +170,25 @@ func TestRunFunction(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - f := &Function{log: logging.NewNopLogger()} + // Mocking the azQuery function to return a successful result + mockQuery := &MockAzureQuery{ + AzQueryFunc: func(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { + return armresourcegraph.ClientResourcesResponse{ + QueryResponse: armresourcegraph.QueryResponse{ + Count: to.Ptr(int64(1)), + Data: []map[string]interface{}{{"resource": "mock-resource"}}, // Mock data + ResultTruncated: to.Ptr(armresourcegraph.ResultTruncatedFalse), + TotalRecords: to.Ptr(int64(1)), + Facets: nil, + SkipToken: nil, + }, + }, nil + }, + } + f := &Function{ + azureQuery: 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 != "" { From 941379052b863467c2f97c0741f857eecec5e3de Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Mon, 6 Jan 2025 14:56:27 +0100 Subject: [PATCH 5/7] Fix issues caught by linter Signed-off-by: Yury Tsarev --- fn.go | 7 ++++--- fn_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/fn.go b/fn.go index 98fb327..10f153b 100644 --- a/fn.go +++ b/fn.go @@ -20,8 +20,9 @@ import ( // TargetXRStatusField is the target field to write the query result to const TargetXRStatusField = "status.azResourceGraphQueryResult" +// AzureQueryInterface defines the methods required for querying Azure resources. type AzureQueryInterface interface { - azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) + azQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) } // Function returns whatever response you ask it to. @@ -66,7 +67,7 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil } - results, err := f.azureQuery.azQuery(ctx, req, azureCreds, in) + results, err := f.azureQuery.azQuery(ctx, azureCreds, in) if err != nil { response.Fatal(rsp, err) f.log.Info("FAILURE: ", "failure", fmt.Sprint(err)) @@ -144,7 +145,7 @@ func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { return azureCreds, nil } -func (f *Function) azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { +func (f *Function) azQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { tenantID := azureCreds["tenantId"] clientID := azureCreds["clientId"] clientSecret := azureCreds["clientSecret"] diff --git a/fn_test.go b/fn_test.go index 91d6eea..f7846c8 100644 --- a/fn_test.go +++ b/fn_test.go @@ -19,11 +19,11 @@ import ( ) type MockAzureQuery struct { - AzQueryFunc func(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) + AzQueryFunc func(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) } -func (m *MockAzureQuery) azQuery(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { - return m.AzQueryFunc(ctx, req, azureCreds, in) +func (m *MockAzureQuery) azQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { + return m.AzQueryFunc(ctx, azureCreds, in) } func TestRunFunction(t *testing.T) { @@ -172,7 +172,7 @@ func TestRunFunction(t *testing.T) { t.Run(name, func(t *testing.T) { // Mocking the azQuery function to return a successful result mockQuery := &MockAzureQuery{ - AzQueryFunc: func(ctx context.Context, req *fnv1.RunFunctionRequest, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { + AzQueryFunc: func(_ context.Context, _ map[string]string, _ *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { return armresourcegraph.ClientResourcesResponse{ QueryResponse: armresourcegraph.QueryResponse{ Count: to.Ptr(int64(1)), From a619ac1c108a3431c1857cc387a38330e3e33d8d Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Mon, 6 Jan 2025 15:53:04 +0100 Subject: [PATCH 6/7] Solve dependency injection issue of real implementation Signed-off-by: Yury Tsarev --- fn.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fn.go b/fn.go index 10f153b..2226c47 100644 --- a/fn.go +++ b/fn.go @@ -67,6 +67,10 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest return rsp, nil } + if f.azureQuery == nil { + f.azureQuery = &AzureQuery{} + } + results, err := f.azureQuery.azQuery(ctx, azureCreds, in) if err != nil { response.Fatal(rsp, err) @@ -145,7 +149,9 @@ func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { return azureCreds, nil } -func (f *Function) azQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { +type AzureQuery struct{} + +func (a *AzureQuery) azQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) { tenantID := azureCreds["tenantId"] clientID := azureCreds["clientId"] clientSecret := azureCreds["clientSecret"] From a5b069cd421e0349dc6ac7c72056989c5d82c346 Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Mon, 6 Jan 2025 16:03:11 +0100 Subject: [PATCH 7/7] Satisfy linter with doc comment Signed-off-by: Yury Tsarev --- fn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fn.go b/fn.go index 2226c47..b9b1cf3 100644 --- a/fn.go +++ b/fn.go @@ -149,6 +149,8 @@ func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { return azureCreds, nil } +// AzureQuery is a concrete implementation of the AzureQueryInterface +// that interacts with Azure Resource Graph API. type AzureQuery struct{} func (a *AzureQuery) azQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (armresourcegraph.ClientResourcesResponse, error) {