Skip to content

Commit

Permalink
First working iteration in a local test. Some TODOs and missing tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
thomas11 committed Mar 7, 2024
1 parent cd8cdc7 commit 600f24b
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 60 deletions.
15 changes: 15 additions & 0 deletions provider/pkg/provider/crud/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type AzureRESTConverter interface {
PrepareAzureRESTBody(id string, inputs resource.PropertyMap) map[string]any

ResponseBodyToSdkOutputs(response map[string]any) map[string]any
ResponseToSdkInputs(pathValues map[string]string, responseBody map[string]any) map[string]any
SdkInputsToRequestBody(values map[string]any, id string) map[string]any
}

// ResourceCrudOperations is an interface for performing CRUD operations on Azure resources of a certain kind.
Expand Down Expand Up @@ -99,6 +101,8 @@ func NewResourceCrudClient(
}
}

type ResourceCrudClientFactory func(res *resources.AzureAPIResource) ResourceCrudClient

func (r *resourceCrudClient) PrepareAzureRESTIdAndQuery(inputs resource.PropertyMap) (string, map[string]any) {
return PrepareAzureRESTIdAndQuery(r.res.Path, r.res.PutParameters, inputs.Mappable(), map[string]any{
"subscriptionId": r.subscriptionID,
Expand Down Expand Up @@ -313,6 +317,17 @@ func (r *resourceCrudClient) ResponseBodyToSdkOutputs(response map[string]any) m
return r.converter.ResponseBodyToSdkOutputs(r.res.Response, response)
}

func (r *resourceCrudClient) ResponseToSdkInputs(pathValues map[string]string, responseBody map[string]any) map[string]any {
return r.converter.ResponseToSdkInputs(r.res.PutParameters, pathValues, responseBody)
}

func (r *resourceCrudClient) SdkInputsToRequestBody(properties map[string]any, id string) map[string]any {
if bodyParam, ok := r.res.BodyParameter(); ok {
return r.converter.SdkInputsToRequestBody(bodyParam.Body.Properties, properties, id)
}
return nil
}

func (r *resourceCrudClient) CanCreate(ctx context.Context, id string) error {
return r.azureClient.CanCreate(ctx, id, r.res.ReadMethod, r.res.APIVersion, r.res.ReadMethod, r.res.Singleton, r.res.DefaultBody != nil, func(outputs map[string]any) bool {
return r.converter.IsDefaultResponse(r.res.PutParameters, outputs, r.res.DefaultBody)
Expand Down
10 changes: 8 additions & 2 deletions provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ func (k *azureNativeProvider) Configure(ctx context.Context,
k.azureClient = azure.NewAzureClient(env, resourceManagerAuth, userAgent)

azCoreTokenCredential := azCoreTokenCredential{p: k}
k.customResources, err = customresources.BuildCustomResources(&env, k.azureClient, k.LookupResource, k.subscriptionID,
var crudClientFactory crud.ResourceCrudClientFactory = func(res *resources.AzureAPIResource) crud.ResourceCrudClient {
return crud.NewResourceCrudClient(k.azureClient, k.lookupType, k.converter, k.subscriptionID, res)
}
k.customResources, err = customresources.BuildCustomResources(&env, k.azureClient, crudClientFactory, k.LookupResource, k.subscriptionID,
resourceManagerBearerAuth, resourceManagerAuth, keyVaultBearerAuth, userAgent, azCoreTokenCredential)
if err != nil {
return nil, fmt.Errorf("initializing custom resources: %w", err)
Expand Down Expand Up @@ -900,6 +903,9 @@ func (k *azureNativeProvider) Create(ctx context.Context, req *rpc.CreateRequest

// Store both outputs and inputs into the state.
obj := checkpointObject(inputs, outputs)
if orig, ok := obj[customresources.OriginalStateKey]; ok {
obj[customresources.OriginalStateKey] = resource.MakeSecret(orig)
}

// Serialize and return RPC outputs
checkpoint, err := plugin.MarshalProperties(
Expand Down Expand Up @@ -1405,7 +1411,7 @@ func (k *azureNativeProvider) Delete(ctx context.Context, req *rpc.DeleteRequest
return nil, errors.Wrapf(err, "resource %s inputs are empty", label)
}
// Our hand-crafted implementation of DELETE operation.
err = customRes.Delete(ctx, id, inputs)
err = customRes.Delete(ctx, id, inputs, state)
if err != nil {
return nil, azure.AzureError(err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func blobContainerLegalHold(azureClient azure.AzureClient) *CustomResource {

return input, nil
},
Delete: func(ctx context.Context, id string, properties resource.PropertyMap) error {
Delete: func(ctx context.Context, id string, properties, state resource.PropertyMap) error {
path := strings.TrimSuffix(id, "/legalHold") + "/clearLegalHold"

tags, err := readTags(properties.Mappable())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func TestDelete(t *testing.T) {
}),
}

err := custom.Delete(context.Background(), containerId+"/legalHold", olds)
err := custom.Delete(context.Background(), containerId+"/legalHold", olds, nil)
require.NoError(t, err)

require.Len(t, m.postIds, 1)
Expand Down
4 changes: 2 additions & 2 deletions provider/pkg/resources/customresources/custom_keyvault.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
func keyVaultSecret(keyVaultDNSSuffix string, kvClient *keyvault.BaseClient) *CustomResource {
return &CustomResource{
path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}",
Delete: func(ctx context.Context, id string, properties resource.PropertyMap) error {
Delete: func(ctx context.Context, id string, properties, state resource.PropertyMap) error {
vaultName := properties["vaultName"]
if !vaultName.HasValue() || !vaultName.IsString() {
return errors.New("vaultName not found in resource state")
Expand All @@ -38,7 +38,7 @@ func keyVaultSecret(keyVaultDNSSuffix string, kvClient *keyvault.BaseClient) *Cu
func keyVaultKey(keyVaultDNSSuffix string, kvClient *keyvault.BaseClient) *CustomResource {
return &CustomResource{
path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{vaultName}/keys/{keyName}",
Delete: func(ctx context.Context, id string, properties resource.PropertyMap) error {
Delete: func(ctx context.Context, id string, properties, state resource.PropertyMap) error {
vaultName := properties["vaultName"]
if !vaultName.HasValue() || !vaultName.IsString() {
return errors.New("vaultName not found in resource state")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func keyVaultAccessPolicy(client *armkeyvault.VaultsClient) *CustomResource {
Update: func(ctx context.Context, id string, properties, olds resource.PropertyMap) (map[string]interface{}, error) {
return c.write(ctx, properties, true /* shouldExist */)
},
Delete: func(ctx context.Context, id string, properties resource.PropertyMap) error {
Delete: func(ctx context.Context, id string, properties, state resource.PropertyMap) error {
return c.modify(ctx, properties, armkeyvault.AccessPolicyUpdateKindRemove)
},
}
Expand Down
108 changes: 62 additions & 46 deletions provider/pkg/resources/customresources/custom_pim.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,38 @@ package customresources

import (
"context"
"fmt"

armauth "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
"github.com/pulumi/pulumi-azure-native/v2/provider/pkg/azure"
"github.com/pulumi/pulumi-azure-native/v2/provider/pkg/provider/crud"
"github.com/pulumi/pulumi-azure-native/v2/provider/pkg/resources"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)

func pimRoleManagementPolicyDirect(azureClient azure.AzureClient) *CustomResource {
const OriginalStateKey = "__orig_state"

func pimRoleManagementPolicy(lookupResource resources.ResourceLookupFunc, crudClientFactory crud.ResourceCrudClientFactory) (*CustomResource, error) {
apiVersion := "2020-10-01" // TODO get from version lock or from CRUD client

// A bit of a hack to initialize some resource. This func's parameters are all nil when the
// function is called for the first time, for customresources.featureLookup, which is ok but
// would break our initialization here.
var client crud.ResourceCrudClient
var res resources.AzureAPIResource
if lookupResource != nil && crudClientFactory != nil {
var found bool
var err error
res, found, err = lookupResource("azure-native:authorization:RoleManagementPolicy")
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("resource %q not found", "azure-native:authorization:RoleManagementPolicy")
}

client = crudClientFactory(&res)
}

return &CustomResource{
path: "/{scope}/providers/Microsoft.Authorization/roleManagementPolicies/{roleManagementPolicyName}",

Expand All @@ -22,55 +47,46 @@ func pimRoleManagementPolicyDirect(azureClient azure.AzureClient) *CustomResourc

// PIM Role Management Policies cannot be created. Instead, each resource has a default policy.
// Simply look up the default policy and return it.
Create: func(ctx context.Context, id string, inputs resource.PropertyMap) (map[string]interface{}, error) {
return azureClient.Get(ctx, id, "2020-10-01" /* TODO from version lock */)
},
}
}
Create: func(ctx context.Context, id string, inputs resource.PropertyMap) (map[string]any, error) {
// First read the existing policy to capture the default for resetting it later on delete.
originalState, err := client.Read(ctx, id)
if err != nil {
return nil, err
}

func pimRoleManagementPolicy(policiesClient *armauth.RoleManagementPoliciesClient) *CustomResource {
client := &pimRoleManagementPolicyClient{
armClient: *policiesClient,
}
bodyParams := client.PrepareAzureRESTBody(id, inputs)
queryParams := map[string]any{"api-version": apiVersion}

return &CustomResource{
path: "/{scope}/providers/Microsoft.Authorization/roleManagementPolicies/{roleManagementPolicyName}",

// PIM Role Management Policies cannot be created. Instead, each resource has a default policy.
// Simply look up the default policy and return it.
Create: func(ctx context.Context, id string, inputs resource.PropertyMap) (map[string]interface{}, error) {
_, err := client.Get(ctx, id)
// TODO we could skip this if bodyParams = originalState, i.e., the user adds a policy
// in its default configuration to their program
resp, _, err := client.CreateOrUpdate(ctx, id, bodyParams, queryParams)
if err != nil {
return nil, err
}
return nil, nil

outputs := client.ResponseBodyToSdkOutputs(resp)
outputs[OriginalStateKey] = originalState
return outputs, nil
},
// Read: func(ctx context.Context, id string, inputs resource.PropertyMap) (map[string]interface{}, bool, error) {
// policy, err := client.Get(ctx, id)
// if err != nil {
// return nil, false, err
// }
// return nil, true, nil
// },
// Update: func(ctx context.Context, id string, news, olds resource.PropertyMap) (map[string]interface{}, error) {
// return nil, nil
// },
// Delete: func(ctx context.Context, id string, properties resource.PropertyMap) error {
// return nil
// },
}
}

type pimRoleManagementPolicyClient struct {
armClient armauth.RoleManagementPoliciesClient
}
// PIM Role Management Policies cannot be deleted. Instead, we reset the policy to its default.
Delete: func(ctx context.Context, id string, properties, state resource.PropertyMap) error {
queryParams := map[string]any{"api-version": apiVersion}
if !state.HasValue(OriginalStateKey) {
logging.V(3).Infof("Warning: no original state found for %s, cannot reset", id)
return nil
}

func (c *pimRoleManagementPolicyClient) Get(ctx context.Context, id string) (*armauth.RoleManagementPolicy, error) {
scope := "TODO"
policyName := "TODO"
resp, err := c.armClient.Get(ctx, scope, policyName, nil)
if err != nil {
return nil, err
}
return &resp.RoleManagementPolicy, nil
origState := state[OriginalStateKey].Mappable().(map[string]any)
pathItems, err := resources.ParseResourceID(id, res.Path)
if err != nil {
return err
}
origSdkInputs := client.ResponseToSdkInputs(pathItems, origState)
origRequest := client.SdkInputsToRequestBody(origSdkInputs, id)

_, _, err = client.CreateOrUpdate(ctx, id, origRequest, queryParams)
return err
},
}, nil
}
4 changes: 2 additions & 2 deletions provider/pkg/resources/customresources/custom_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (r *staticWebsite) read(ctx context.Context, id string, properties resource
return outputs, true, nil
}

func (r *staticWebsite) delete(ctx context.Context, id string, properties resource.PropertyMap) error {
func (r *staticWebsite) delete(ctx context.Context, id string, properties, state resource.PropertyMap) error {
dataClient, found, err := r.newDataClient(ctx, properties)
if err != nil {
return err
Expand Down Expand Up @@ -531,7 +531,7 @@ func (r *blob) update(ctx context.Context, id string, properties, oldState resou
return state, err
}

func (r *blob) delete(ctx context.Context, id string, properties resource.PropertyMap) error {
func (r *blob) delete(ctx context.Context, id string, properties, state resource.PropertyMap) error {
blobsClient, found, err := r.newDataClient(ctx, properties)
if err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func customWebAppDelete(lookupResource resources.ResourceLookupFunc, azureDelete
return &CustomResource{
path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{name}",
tok: webAppResourceType,
Delete: func(ctx context.Context, id string, properties resource.PropertyMap) error {
Delete: func(ctx context.Context, id string, properties, state resource.PropertyMap) error {
res, ok, err := lookupResource(webAppResourceType)
if err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (t *MockAzureDeleter) Delete(ctx context.Context, id, apiVersion, asyncStyl
func TestSetsDeleteParam(t *testing.T) {
deleter := MockAzureDeleter{}
custom := customWebAppDelete(mockResourceLookup, &deleter)
custom.Delete(context.Background(), "id", resource.PropertyMap{})
custom.Delete(context.Background(), "id", resource.PropertyMap{}, resource.PropertyMap{})
assert.Len(t, deleter.queryParamsOfLastDelete, 1)
assert.Contains(t, deleter.queryParamsOfLastDelete, "deleteEmptyServerFarm")
}
13 changes: 10 additions & 3 deletions provider/pkg/resources/customresources/customresources.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/Azure/go-autorest/autorest"
azureEnv "github.com/Azure/go-autorest/autorest/azure"
"github.com/pulumi/pulumi-azure-native/v2/provider/pkg/azure"
"github.com/pulumi/pulumi-azure-native/v2/provider/pkg/provider/crud"
. "github.com/pulumi/pulumi-azure-native/v2/provider/pkg/resources"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
Expand All @@ -39,12 +40,13 @@ type CustomResource struct {
// Update an existing resource with a map of input values. Returns a map of resource outputs that match the schema shape.
Update func(ctx context.Context, id string, news, olds resource.PropertyMap) (map[string]interface{}, error)
// Delete an existing resource. Constructs the resource ID based on input values.
Delete func(ctx context.Context, id string, properties resource.PropertyMap) error
Delete func(ctx context.Context, id string, previousInputs, state resource.PropertyMap) error
}

// BuildCustomResources creates a map of custom resources for given environment parameters.
func BuildCustomResources(env *azureEnv.Environment,
azureClient azure.AzureClient,
crudClientFactory crud.ResourceCrudClientFactory,
lookupResource ResourceLookupFunc,
subscriptionID string,
bearerAuth autorest.Authorizer,
Expand All @@ -66,6 +68,11 @@ func BuildCustomResources(env *azureEnv.Environment,
storageAccountsClient.Authorizer = tokenAuth
storageAccountsClient.UserAgent = userAgent

pimRoleManagementPolicy, err := pimRoleManagementPolicy(lookupResource, crudClientFactory)
if err != nil {
return nil, err
}

resources := []*CustomResource{
// Azure KeyVault resources.
keyVaultSecret(env.KeyVaultDNSSuffix, &kvClient),
Expand All @@ -77,7 +84,7 @@ func BuildCustomResources(env *azureEnv.Environment,
// Customization of regular resources
customWebAppDelete(lookupResource, azureClient),
blobContainerLegalHold(azureClient),
pimRoleManagementPolicyDirect(azureClient),
pimRoleManagementPolicy,
}

result := map[string]*CustomResource{}
Expand All @@ -88,7 +95,7 @@ func BuildCustomResources(env *azureEnv.Environment,
}

// featureLookup is a map of custom resource to lookup their capabilities.
var featureLookup, _ = BuildCustomResources(&azureEnv.Environment{}, nil, nil, "", nil, nil, nil, "", nil)
var featureLookup, _ = BuildCustomResources(&azureEnv.Environment{}, nil, nil, nil, "", nil, nil, nil, "", nil)

func IsCustomResource(path string) bool {
_, ok := featureLookup[path]
Expand Down

0 comments on commit 600f24b

Please sign in to comment.