diff --git a/provider/v2/azure/config.go b/provider/v2/azure/config.go new file mode 100644 index 000000000..eb2927001 --- /dev/null +++ b/provider/v2/azure/config.go @@ -0,0 +1,140 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 azure + +import ( + "encoding/base64" + "errors" + "fmt" + + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" +) + +const ( + DefaultEnvPrefix = "VMCLARITY_AZURE" +) + +type AzurePublicKey string + +func (a *AzurePublicKey) UnmarshalText(text []byte) error { + if len(text) != 0 { + publicKey, err := base64.StdEncoding.DecodeString(string(text)) + if err != nil { + return fmt.Errorf("failed to decode azure scanner public key from base64: %w", err) + } + *a = AzurePublicKey(publicKey) + } + return nil +} + +type Config struct { + SubscriptionID string `mapstructure:"subscription_id"` + ScannerLocation string `mapstructure:"scanner_location"` + ScannerResourceGroup string `mapstructure:"scanner_resource_group"` + ScannerSubnet string `mapstructure:"scanner_subnet_id"` + ScannerPublicKey AzurePublicKey `mapstructure:"scanner_public_key"` + ScannerVMSize string `mapstructure:"scanner_vm_size"` + ScannerImagePublisher string `mapstructure:"scanner_image_publisher"` + ScannerImageOffer string `mapstructure:"scanner_image_offer"` + ScannerImageSKU string `mapstructure:"scanner_image_sku"` + ScannerImageVersion string `mapstructure:"scanner_image_version"` + ScannerSecurityGroup string `mapstructure:"scanner_security_group"` + ScannerStorageAccountName string `mapstructure:"scanner_storage_account_name"` + ScannerStorageContainerName string `mapstructure:"scanner_storage_container_name"` +} + +func NewConfig() (*Config, error) { + // Avoid modifying the global instance + v := viper.New() + + v.SetEnvPrefix(DefaultEnvPrefix) + v.AllowEmptyEnv(true) + v.AutomaticEnv() + + _ = v.BindEnv("subscription_id") + _ = v.BindEnv("scanner_location") + _ = v.BindEnv("scanner_resource_group") + _ = v.BindEnv("scanner_subnet_id") + _ = v.BindEnv("scanner_public_key") + _ = v.BindEnv("scanner_vm_size") + _ = v.BindEnv("scanner_image_publisher") + _ = v.BindEnv("scanner_image_offer") + _ = v.BindEnv("scanner_image_sku") + _ = v.BindEnv("scanner_image_version") + _ = v.BindEnv("scanner_security_group") + _ = v.BindEnv("scanner_storage_account_name") + _ = v.BindEnv("scanner_storage_container_name") + + config := &Config{} + if err := v.Unmarshal(&config, viper.DecodeHook(mapstructure.TextUnmarshallerHookFunc())); err != nil { + return nil, fmt.Errorf("failed to parse provider configuration. Provider=Azure: %w", err) + } + return config, nil +} + +// nolint:cyclop +func (c Config) Validate() error { + if c.SubscriptionID == "" { + return errors.New("parameter SubscriptionID must be provided") + } + + if c.ScannerLocation == "" { + return errors.New("parameter ScannerLocation must be provided") + } + + if c.ScannerResourceGroup == "" { + return errors.New("parameter ScannerResourceGroup must be provided") + } + + if c.ScannerSubnet == "" { + return errors.New("parameter ScannerSubnet must be provided") + } + + if c.ScannerVMSize == "" { + return errors.New("parameter ScannerVMSize must be provided") + } + + if c.ScannerImagePublisher == "" { + return errors.New("parameter ScannerImagePublisher must be provided") + } + + if c.ScannerImageOffer == "" { + return errors.New("parameter ScannerImageOffer must be provided") + } + + if c.ScannerImageSKU == "" { + return errors.New("parameter ScannerImageSKU must be provided") + } + + if c.ScannerImageVersion == "" { + return errors.New("parameter ScannerImageVersion must be provided") + } + + if c.ScannerSecurityGroup == "" { + return errors.New("parameter ScannerSecurityGroup must be provided") + } + + if c.ScannerStorageAccountName == "" { + return errors.New("parameter ScannerStorageAccountName must be provided") + } + + if c.ScannerStorageContainerName == "" { + return errors.New("parameter ScannerStorageContainerName must be provided") + } + + return nil +} diff --git a/provider/v2/azure/discoverer/discoverer.go b/provider/v2/azure/discoverer/discoverer.go index 03706bb43..a936169d5 100644 --- a/provider/v2/azure/discoverer/discoverer.go +++ b/provider/v2/azure/discoverer/discoverer.go @@ -17,15 +17,142 @@ package discoverer import ( "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/core/log" + "github.com/openclarity/vmclarity/core/to" "github.com/openclarity/vmclarity/provider" ) -var _ provider.Discoverer = &Discoverer{} - -type Discoverer struct{} +type Discoverer struct { + VMClient *armcompute.VirtualMachinesClient + DisksClient *armcompute.DisksClient +} func (d *Discoverer) DiscoverAssets(ctx context.Context) provider.AssetDiscoverer { - // TODO implement me - panic("implement me") + assetDiscoverer := provider.NewSimpleAssetDiscoverer() + + go func() { + defer close(assetDiscoverer.OutputChan) + + // list all vms in all resourceGroups in the subscription + res := d.VMClient.NewListAllPager(nil) + for res.More() { + page, err := res.NextPage(ctx) + if err != nil { + assetDiscoverer.Error = fmt.Errorf("failed to get next page: %w", err) + return + } + ts, err := d.processVirtualMachineListIntoAssetTypes(ctx, page.VirtualMachineListResult) + if err != nil { + assetDiscoverer.Error = err + return + } + + for _, asset := range ts { + select { + case assetDiscoverer.OutputChan <- asset: + case <-ctx.Done(): + assetDiscoverer.Error = ctx.Err() + return + } + } + } + }() + + return assetDiscoverer +} + +func (d *Discoverer) processVirtualMachineListIntoAssetTypes(ctx context.Context, vmList armcompute.VirtualMachineListResult) ([]apitypes.AssetType, error) { + ret := make([]apitypes.AssetType, 0, len(vmList.Value)) + for _, vm := range vmList.Value { + info, err := getVMInfoFromVirtualMachine(vm, d.getRootVolumeInfo(ctx, vm)) + if err != nil { + return nil, fmt.Errorf("unable to convert instance to vminfo: %w", err) + } + ret = append(ret, info) + } + return ret, nil +} + +func (d *Discoverer) getRootVolumeInfo(ctx context.Context, vm *armcompute.VirtualMachine) *apitypes.RootVolume { + logger := log.GetLoggerFromContextOrDiscard(ctx) + ret := &apitypes.RootVolume{ + SizeGB: int(to.ValueOrZero(vm.Properties.StorageProfile.OSDisk.DiskSizeGB)), + Encrypted: apitypes.RootVolumeEncryptedUnknown, + } + osDiskID, err := arm.ParseResourceID(to.ValueOrZero(vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID)) + if err != nil { + logger.Warnf("Failed to parse disk ID. DiskID=%v: %v", + to.ValueOrZero(vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID), err) + return ret + } + osDisk, err := d.DisksClient.Get(ctx, osDiskID.ResourceGroupName, osDiskID.Name, nil) + if err != nil { + logger.Warnf("Failed to get OS disk. DiskID=%v: %v", + to.ValueOrZero(vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID), err) + return ret + } + ret.Encrypted = isEncrypted(osDisk) + ret.SizeGB = int(to.ValueOrZero(osDisk.Disk.Properties.DiskSizeGB)) + + return ret +} + +func getVMInfoFromVirtualMachine(vm *armcompute.VirtualMachine, rootVol *apitypes.RootVolume) (apitypes.AssetType, error) { + assetType := apitypes.AssetType{} + err := assetType.FromVMInfo(apitypes.VMInfo{ + ObjectType: "VMInfo", + InstanceProvider: to.Ptr(apitypes.Azure), + InstanceID: *vm.ID, + Image: createImageURN(vm.Properties.StorageProfile.ImageReference), + InstanceType: *vm.Type, + LaunchTime: *vm.Properties.TimeCreated, + Location: *vm.Location, + Platform: string(*vm.Properties.StorageProfile.OSDisk.OSType), + RootVolume: *rootVol, + SecurityGroups: &[]apitypes.SecurityGroup{}, + Tags: convertTags(vm.Tags), + }) + if err != nil { + err = fmt.Errorf("failed to create AssetType from VMInfo: %w", err) + } + + return assetType, err +} + +func isEncrypted(disk armcompute.DisksClientGetResponse) apitypes.RootVolumeEncrypted { + if disk.Properties.EncryptionSettingsCollection == nil { + return apitypes.RootVolumeEncryptedNo + } + if *disk.Properties.EncryptionSettingsCollection.Enabled { + return apitypes.RootVolumeEncryptedYes + } + + return apitypes.RootVolumeEncryptedNo +} + +func convertTags(tags map[string]*string) *[]apitypes.Tag { + ret := make([]apitypes.Tag, 0, len(tags)) + for key, val := range tags { + ret = append(ret, apitypes.Tag{ + Key: key, + Value: *val, + }) + } + return &ret +} + +// https://learn.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-manage-vm#understand-vm-images +func createImageURN(reference *armcompute.ImageReference) string { + // ImageReference is required only when using platform images, marketplace images, or + // virtual machine images, but is not used in other creation operations (like managed disks). + if reference == nil { + return "" + } + return *reference.Publisher + "/" + *reference.Offer + "/" + *reference.SKU + "/" + *reference.Version } diff --git a/provider/v2/azure/discoverer/discoverer_test.go b/provider/v2/azure/discoverer/discoverer_test.go new file mode 100644 index 000000000..27498f615 --- /dev/null +++ b/provider/v2/azure/discoverer/discoverer_test.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 discoverer + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + + apitypes "github.com/openclarity/vmclarity/api/types" + "github.com/openclarity/vmclarity/core/to" +) + +func Test_isEncrypted(t *testing.T) { + type args struct { + disk armcompute.DisksClientGetResponse + } + tests := []struct { + name string + args args + want apitypes.RootVolumeEncrypted + }{ + { + name: "encrypted", + args: args{ + disk: armcompute.DisksClientGetResponse{ + Disk: armcompute.Disk{ + Properties: &armcompute.DiskProperties{ + EncryptionSettingsCollection: &armcompute.EncryptionSettingsCollection{ + Enabled: to.Ptr(true), + }, + }, + }, + }, + }, + want: apitypes.RootVolumeEncryptedYes, + }, + { + name: "not encrypted", + args: args{ + disk: armcompute.DisksClientGetResponse{ + Disk: armcompute.Disk{ + Properties: &armcompute.DiskProperties{ + EncryptionSettingsCollection: nil, + }, + }, + }, + }, + want: apitypes.RootVolumeEncryptedNo, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isEncrypted(tt.args.disk); got != tt.want { + t.Errorf("isEncrypted() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/provider/v2/azure/estimator/estimator.go b/provider/v2/azure/estimator/estimator.go index 5b79b9844..1d7bf689e 100644 --- a/provider/v2/azure/estimator/estimator.go +++ b/provider/v2/azure/estimator/estimator.go @@ -22,11 +22,8 @@ import ( "github.com/openclarity/vmclarity/provider" ) -var _ provider.Estimator = &Estimator{} - type Estimator struct{} func (e *Estimator) Estimate(ctx context.Context, stats apitypes.AssetScanStats, asset *apitypes.Asset, template *apitypes.AssetScanTemplate) (*apitypes.Estimation, error) { - // TODO implement me - panic("implement me") + return &apitypes.Estimation{}, provider.FatalErrorf("Not Implemented") } diff --git a/provider/v2/azure/provider.go b/provider/v2/azure/provider.go index f09cc0b3d..0ab62b956 100644 --- a/provider/v2/azure/provider.go +++ b/provider/v2/azure/provider.go @@ -17,16 +17,18 @@ package azure import ( "context" + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" apitypes "github.com/openclarity/vmclarity/api/types" - "github.com/openclarity/vmclarity/provider" - "github.com/openclarity/vmclarity/provider/v2/aws/discoverer" - "github.com/openclarity/vmclarity/provider/v2/aws/estimator" - "github.com/openclarity/vmclarity/provider/v2/aws/scanner" + "github.com/openclarity/vmclarity/provider/v2/azure/discoverer" + "github.com/openclarity/vmclarity/provider/v2/azure/estimator" + "github.com/openclarity/vmclarity/provider/v2/azure/scanner" ) -var _ provider.Provider = &Provider{} - type Provider struct { *discoverer.Discoverer *scanner.Scanner @@ -34,14 +36,61 @@ type Provider struct { } func (p *Provider) Kind() apitypes.CloudProvider { - // TODO implement me - panic("implement me") + return apitypes.Azure } func New(_ context.Context) (*Provider, error) { + config, err := NewConfig() + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %w", err) + } + + err = config.Validate() + if err != nil { + return nil, fmt.Errorf("failed to validate configuration: %w", err) + } + + cred, err := azidentity.NewManagedIdentityCredential(nil) + if err != nil { + return nil, fmt.Errorf("failed create managed identity credential: %w", err) + } + + networkClientFactory, err := armnetwork.NewClientFactory(config.SubscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create network client factory: %w", err) + } + + computeClientFactory, err := armcompute.NewClientFactory(config.SubscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create compute client factory: %w", err) + } + return &Provider{ - Discoverer: &discoverer.Discoverer{}, - Scanner: &scanner.Scanner{}, - Estimator: &estimator.Estimator{}, + Discoverer: &discoverer.Discoverer{ + VMClient: computeClientFactory.NewVirtualMachinesClient(), + DisksClient: computeClientFactory.NewDisksClient(), + }, + Scanner: &scanner.Scanner{ + Cred: cred, + VMClient: computeClientFactory.NewVirtualMachinesClient(), + SnapshotsClient: computeClientFactory.NewSnapshotsClient(), + DisksClient: computeClientFactory.NewDisksClient(), + InterfacesClient: networkClientFactory.NewInterfacesClient(), + + SubscriptionID: config.SubscriptionID, + ScannerLocation: config.ScannerLocation, + ScannerResourceGroup: config.ScannerResourceGroup, + ScannerSubnet: config.ScannerSubnet, + ScannerPublicKey: string(config.ScannerPublicKey), + ScannerVMSize: config.ScannerVMSize, + ScannerImagePublisher: config.ScannerImagePublisher, + ScannerImageOffer: config.ScannerImageOffer, + ScannerImageSKU: config.ScannerImageSKU, + ScannerImageVersion: config.ScannerImageVersion, + ScannerSecurityGroup: config.ScannerSecurityGroup, + ScannerStorageAccountName: config.ScannerStorageAccountName, + ScannerStorageContainerName: config.ScannerStorageContainerName, + }, + Estimator: &estimator.Estimator{}, }, nil } diff --git a/provider/v2/azure/scanner/blob.go b/provider/v2/azure/scanner/blob.go new file mode 100644 index 000000000..ff3bd92d6 --- /dev/null +++ b/provider/v2/azure/scanner/blob.go @@ -0,0 +1,147 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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. + +// nolint: wrapcheck +package scanner + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob" + + "github.com/openclarity/vmclarity/core/to" + "github.com/openclarity/vmclarity/provider" + "github.com/openclarity/vmclarity/provider/v2/azure/utils" +) + +var ( + estimatedBlobCopyTime = 2 * time.Minute + estimatedBlobAbortTime = 2 * time.Minute + estimatedBlobDeleteTime = 2 * time.Minute + snapshotSASAccessSeconds = 3600 +) + +func blobNameFromJobConfig(config *provider.ScanJobConfig) string { + return config.AssetScanID + ".vhd" +} + +func (s *Scanner) blobURLFromBlobName(blobName string) string { + return fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", s.ScannerStorageAccountName, s.ScannerStorageContainerName, blobName) +} + +func (s *Scanner) ensureBlobFromSnapshot(ctx context.Context, config *provider.ScanJobConfig, snapshot armcompute.Snapshot) (string, error) { + blobName := blobNameFromJobConfig(config) + blobURL := s.blobURLFromBlobName(blobName) + blobClient, err := blob.NewClient(blobURL, s.Cred, nil) + if err != nil { + return blobURL, provider.FatalErrorf("failed to init blob client: %w", err) + } + + getMetadata, err := blobClient.GetProperties(ctx, nil) + if err == nil { + copyStatus := *getMetadata.CopyStatus + if copyStatus != blob.CopyStatusTypeSuccess { + log.Print("blob is still copying, status is ", copyStatus) + return blobURL, provider.RetryableErrorf(estimatedBlobCopyTime, "blob is still copying") + } + + revokepoller, err := s.SnapshotsClient.BeginRevokeAccess(ctx, s.ScannerResourceGroup, *snapshot.Name, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "revoking SAS access for snapshot %s", *snapshot.Name) + return blobURL, err + } + _, err = revokepoller.PollUntilDone(ctx, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "waiting for SAS access to be revoked for snapshot %s", *snapshot.Name) + return blobURL, err + } + + return blobURL, nil + } + + notFound, err := utils.HandleAzureRequestError(err, "getting blob %s", blobName) + if !notFound { + return blobURL, err + } + + // NOTE(sambetts) Granting SAS access to a snapshot must be done + // atomically with starting the CopyFromUrl Operation because + // GrantAccess only provides the URL once, and we don't want to store + // it. + poller, err := s.SnapshotsClient.BeginGrantAccess(ctx, s.ScannerResourceGroup, *snapshot.Name, armcompute.GrantAccessData{ + Access: to.Ptr(armcompute.AccessLevelRead), + DurationInSeconds: to.Ptr[int32](int32(snapshotSASAccessSeconds)), + }, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "granting SAS access to snapshot %s", *snapshot.Name) + return blobURL, err + } + + res, err := poller.PollUntilDone(ctx, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "waiting for SAS access to snapshot %s be granted", *snapshot.Name) + return blobURL, err + } + + accessURL := *res.AccessURI.AccessSAS + + _, err = blobClient.StartCopyFromURL(ctx, accessURL, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "starting copy from URL operation for blob %s", blobName) + return blobURL, err + } + + return blobURL, provider.RetryableErrorf(estimatedBlobCopyTime, "blob copy from url started") +} + +func (s *Scanner) ensureBlobDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + blobName := blobNameFromJobConfig(config) + blobURL := s.blobURLFromBlobName(blobName) + blobClient, err := blob.NewClient(blobURL, s.Cred, nil) + if err != nil { + return provider.FatalErrorf("failed to init blob client: %w", err) + } + + getMetadata, err := blobClient.GetProperties(ctx, nil) + if err != nil { + notFound, err := utils.HandleAzureRequestError(err, "getting blob %s", blobName) + if notFound { + return nil + } + return err + } + + copyStatus := *getMetadata.CopyStatus + if copyStatus == blob.CopyStatusTypePending { + _, err = blobClient.AbortCopyFromURL(ctx, *getMetadata.CopyID, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "aborting copy from url for blob %s", blobName) + return err + } + return provider.RetryableErrorf(estimatedBlobAbortTime, "blob copy aborting") + } + + _, err = blobClient.Delete(ctx, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "deleting blob %s", blobName) + return err + } + + return provider.RetryableErrorf(estimatedBlobDeleteTime, "blob %s delete started", blobName) +} diff --git a/provider/v2/azure/scanner/networkInterface.go b/provider/v2/azure/scanner/networkInterface.go new file mode 100644 index 000000000..0179cb2f2 --- /dev/null +++ b/provider/v2/azure/scanner/networkInterface.go @@ -0,0 +1,100 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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. + +// nolint: wrapcheck +package scanner + +import ( + "context" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" + + "github.com/openclarity/vmclarity/core/to" + "github.com/openclarity/vmclarity/provider" + "github.com/openclarity/vmclarity/provider/v2/azure/utils" +) + +var ( + NetworkInterfaceEstimateProvisionTime = 10 * time.Second + NetworkInterfaceDeleteEstimateTime = 10 * time.Second +) + +func networkInterfaceNameFromJobConfig(config *provider.ScanJobConfig) string { + return "scanner-nic-" + config.AssetScanID +} + +func (s *Scanner) ensureNetworkInterface(ctx context.Context, config *provider.ScanJobConfig) (armnetwork.Interface, error) { + nicName := networkInterfaceNameFromJobConfig(config) + + nicResp, err := s.InterfacesClient.Get(ctx, s.ScannerResourceGroup, nicName, nil) + if err == nil { + if *nicResp.Interface.Properties.ProvisioningState != provisioningStateSucceeded { + return nicResp.Interface, provider.RetryableErrorf(NetworkInterfaceEstimateProvisionTime, "interface is not ready yet, provisioning state: %s", *nicResp.Interface.Properties.ProvisioningState) + } + + return nicResp.Interface, nil + } + + notFound, err := utils.HandleAzureRequestError(err, "getting interface %s", nicName) + if !notFound { + return armnetwork.Interface{}, err + } + + parameters := armnetwork.Interface{ + Location: to.Ptr(s.ScannerLocation), + Properties: &armnetwork.InterfacePropertiesFormat{ + IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ + { + Name: to.Ptr(nicName + "-ipconfig"), + Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ + PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + Subnet: &armnetwork.Subnet{ + ID: to.Ptr(s.ScannerSubnet), + }, + }, + }, + }, + NetworkSecurityGroup: &armnetwork.SecurityGroup{ + ID: to.Ptr(s.ScannerSecurityGroup), + }, + }, + } + + _, err = s.InterfacesClient.BeginCreateOrUpdate(ctx, s.ScannerResourceGroup, nicName, parameters, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "creating interface %s", nicName) + return armnetwork.Interface{}, err + } + + return armnetwork.Interface{}, provider.RetryableErrorf(NetworkInterfaceEstimateProvisionTime, "interface creating") +} + +func (s *Scanner) ensureNetworkInterfaceDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + nicName := networkInterfaceNameFromJobConfig(config) + + return utils.EnsureDeleted( + "interface", + func() error { + _, err := s.InterfacesClient.Get(ctx, s.ScannerResourceGroup, nicName, nil) + return err + }, + func() error { + _, err := s.InterfacesClient.BeginDelete(ctx, s.ScannerResourceGroup, nicName, nil) + return err + }, + NetworkInterfaceDeleteEstimateTime, + ) +} diff --git a/provider/v2/azure/scanner/scanner.go b/provider/v2/azure/scanner/scanner.go index 9a6064884..d6623f421 100644 --- a/provider/v2/azure/scanner/scanner.go +++ b/provider/v2/azure/scanner/scanner.go @@ -13,24 +13,143 @@ // See the License for the specific language governing permissions and // limitations under the License. +// nolint: wrapcheck package scanner import ( "context" + "fmt" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" "github.com/openclarity/vmclarity/provider" + "github.com/openclarity/vmclarity/provider/v2/azure/utils" ) -var _ provider.Scanner = &Scanner{} +const ( + provisioningStateSucceeded = "Succeeded" + instanceIDPartsLength = 9 + resourceGroupPartIdx = 4 + vmNamePartIdx = 8 +) -type Scanner struct{} +type Scanner struct { + Cred azcore.TokenCredential + VMClient *armcompute.VirtualMachinesClient + SnapshotsClient *armcompute.SnapshotsClient + DisksClient *armcompute.DisksClient + InterfacesClient *armnetwork.InterfacesClient -func (s *Scanner) RunAssetScan(ctx context.Context, t *provider.ScanJobConfig) error { - // TODO implement me - panic("implement me") + SubscriptionID string + ScannerLocation string + ScannerResourceGroup string + ScannerSubnet string + ScannerPublicKey string + ScannerVMSize string + ScannerImagePublisher string + ScannerImageOffer string + ScannerImageSKU string + ScannerImageVersion string + ScannerSecurityGroup string + ScannerStorageAccountName string + ScannerStorageContainerName string } -func (s *Scanner) RemoveAssetScan(ctx context.Context, t *provider.ScanJobConfig) error { - // TODO implement me - panic("implement me") +// nolint:cyclop +func (s *Scanner) RunAssetScan(ctx context.Context, config *provider.ScanJobConfig) error { + vmInfo, err := config.AssetInfo.AsVMInfo() + if err != nil { + return provider.FatalErrorf("unable to get vminfo from asset: %w", err) + } + + resourceGroup, vmName, err := resourceGroupAndNameFromInstanceID(vmInfo.InstanceID) + if err != nil { + return err + } + + assetVM, err := s.VMClient.Get(ctx, resourceGroup, vmName, nil) + if err != nil { + _, err = utils.HandleAzureRequestError(err, "getting asset virtual machine %s", vmName) + return err + } + + snapshot, err := s.ensureSnapshotForVMRootVolume(ctx, config, assetVM.VirtualMachine) + if err != nil { + return fmt.Errorf("failed to ensure snapshot for vm root volume: %w", err) + } + + var disk armcompute.Disk + if *assetVM.Location == s.ScannerLocation { + disk, err = s.ensureManagedDiskFromSnapshot(ctx, config, snapshot) + if err != nil { + return fmt.Errorf("failed to ensure managed disk created from snapshot: %w", err) + } + } else { + disk, err = s.ensureManagedDiskFromSnapshotInDifferentRegion(ctx, config, snapshot) + if err != nil { + return fmt.Errorf("failed to ensure managed disk from snapshot in different region: %w", err) + } + } + + networkInterface, err := s.ensureNetworkInterface(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure scanner network interface: %w", err) + } + + scannerVM, err := s.ensureScannerVirtualMachine(ctx, config, networkInterface) + if err != nil { + return fmt.Errorf("failed to ensure scanner virtual machine: %w", err) + } + + err = s.ensureDiskAttachedToScannerVM(ctx, scannerVM, disk) + if err != nil { + return fmt.Errorf("failed to ensure asset disk is attached to virtual machine: %w", err) + } + + return nil +} + +func (s *Scanner) RemoveAssetScan(ctx context.Context, config *provider.ScanJobConfig) error { + err := s.ensureScannerVirtualMachineDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure scanner virtual machine deleted: %w", err) + } + + err = s.ensureNetworkInterfaceDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure network interface deleted: %w", err) + } + + err = s.ensureTargetDiskDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure asset disk deleted: %w", err) + } + + err = s.ensureBlobDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure snapshot copy blob deleted: %w", err) + } + + err = s.ensureSnapshotDeleted(ctx, config) + if err != nil { + return fmt.Errorf("failed to ensure snapshot deleted: %w", err) + } + + return nil +} + +// Example Instance ID: +// +// /subscriptions/ecad88af-09d5-4725-8d80-906e51fddf02/resourceGroups/vmclarity-sambetts-dev/providers/Microsoft.Compute/virtualMachines/vmclarity-server +// +// Will return "vmclarity-sambetts-dev" and "vmclarity-server". +func resourceGroupAndNameFromInstanceID(instanceID string) (string, string, error) { + idParts := strings.Split(instanceID, "/") + if len(idParts) != instanceIDPartsLength { + return "", "", provider.FatalErrorf("asset instance id in unexpected format got: %s", idParts) + } + return idParts[resourceGroupPartIdx], idParts[vmNamePartIdx], nil } diff --git a/provider/v2/azure/scanner/scannerVm.go b/provider/v2/azure/scanner/scannerVm.go new file mode 100644 index 000000000..ad15f423b --- /dev/null +++ b/provider/v2/azure/scanner/scannerVm.go @@ -0,0 +1,190 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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. + +// nolint:wrapcheck +package scanner + +import ( + "context" + "encoding/base64" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v5" + + "github.com/openclarity/vmclarity/core/to" + "github.com/openclarity/vmclarity/provider" + "github.com/openclarity/vmclarity/provider/cloudinit" + "github.com/openclarity/vmclarity/provider/v2/azure/utils" +) + +var ( + VMCreateEstimateProvisionTime = 2 * time.Minute + VMDiskAttachEstimateTime = 2 * time.Minute + VMDeleteEstimateTime = 2 * time.Minute +) + +func scannerVMNameFromJobConfig(config *provider.ScanJobConfig) string { + return "vmclarity-scanner-" + config.AssetScanID +} + +func (s *Scanner) ensureScannerVirtualMachine(ctx context.Context, config *provider.ScanJobConfig, networkInterface armnetwork.Interface) (armcompute.VirtualMachine, error) { + vmName := scannerVMNameFromJobConfig(config) + + vmResp, err := s.VMClient.Get(ctx, s.ScannerResourceGroup, vmName, nil) + if err == nil { + if *vmResp.VirtualMachine.Properties.ProvisioningState != provisioningStateSucceeded { + return vmResp.VirtualMachine, provider.RetryableErrorf(VMCreateEstimateProvisionTime, "VM is not ready yet, provisioning state: %s", *vmResp.VirtualMachine.Properties.ProvisioningState) + } + return vmResp.VirtualMachine, nil + } + + notFound, err := utils.HandleAzureRequestError(err, "getting scanner virtual machine: %s", vmName) + if !notFound { + return armcompute.VirtualMachine{}, err + } + + userData, err := cloudinit.New(config) + if err != nil { + return armcompute.VirtualMachine{}, fmt.Errorf("failed to generate cloud-init: %w", err) + } + userDataBase64 := base64.StdEncoding.EncodeToString([]byte(userData)) + + parameters := armcompute.VirtualMachine{ + Location: to.Ptr(s.ScannerLocation), + Identity: &armcompute.VirtualMachineIdentity{ + // Scanners don't need access to Azure so no need for an Identity + Type: to.Ptr(armcompute.ResourceIdentityTypeNone), + }, + Properties: &armcompute.VirtualMachineProperties{ + HardwareProfile: &armcompute.HardwareProfile{ + VMSize: to.Ptr(armcompute.VirtualMachineSizeTypes(s.ScannerVMSize)), + }, + StorageProfile: &armcompute.StorageProfile{ + ImageReference: &armcompute.ImageReference{ + Publisher: to.Ptr(s.ScannerImagePublisher), + SKU: to.Ptr(s.ScannerImageSKU), + Version: to.Ptr(s.ScannerImageVersion), + Offer: to.Ptr(s.ScannerImageOffer), + }, + OSDisk: &armcompute.OSDisk{ + Name: to.Ptr(vmName + "-rootvolume"), + CreateOption: to.Ptr(armcompute.DiskCreateOptionTypesFromImage), + // Delete disk on VM delete + DeleteOption: to.Ptr(armcompute.DiskDeleteOptionTypesDelete), + Caching: to.Ptr(armcompute.CachingTypesReadWrite), + ManagedDisk: &armcompute.ManagedDiskParameters{ + // OSDisk type Standard/Premium HDD/SSD + StorageAccountType: to.Ptr(armcompute.StorageAccountTypesStandardLRS), + }, + // DiskSizeGB: to.Ptr[int32](100), // default 127G + }, + }, + OSProfile: &armcompute.OSProfile{ // use username/password + ComputerName: to.Ptr(vmName), + AdminUsername: to.Ptr("vmclarity"), + LinuxConfiguration: &armcompute.LinuxConfiguration{ + DisablePasswordAuthentication: to.Ptr(true), + }, + }, + NetworkProfile: &armcompute.NetworkProfile{ + NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ + { + ID: networkInterface.ID, + }, + }, + }, + UserData: &userDataBase64, + }, + } + + if s.ScannerPublicKey != "" { + parameters.Properties.OSProfile.LinuxConfiguration.SSH = &armcompute.SSHConfiguration{ + PublicKeys: []*armcompute.SSHPublicKey{ + { + Path: to.Ptr(fmt.Sprintf("/home/%s/.ssh/authorized_keys", "vmclarity")), + KeyData: to.Ptr(s.ScannerPublicKey), + }, + }, + } + } + + _, err = s.VMClient.BeginCreateOrUpdate(ctx, s.ScannerResourceGroup, vmName, parameters, nil) + if err != nil { + _, err = utils.HandleAzureRequestError(err, "creating virtual machine") + return armcompute.VirtualMachine{}, err + } + + return armcompute.VirtualMachine{}, provider.RetryableErrorf(VMCreateEstimateProvisionTime, "vm created") +} + +func (s *Scanner) ensureScannerVirtualMachineDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + vmName := scannerVMNameFromJobConfig(config) + + return utils.EnsureDeleted( + "virtual machine", + func() error { + _, err := s.VMClient.Get(ctx, s.ScannerResourceGroup, vmName, nil) + return err + }, + func() error { + _, err := s.VMClient.BeginDelete(ctx, s.ScannerResourceGroup, vmName, nil) + return err + }, + VMDeleteEstimateTime, + ) +} + +func (s *Scanner) ensureDiskAttachedToScannerVM(ctx context.Context, vm armcompute.VirtualMachine, disk armcompute.Disk) error { + var vmAttachedToDisk bool + for _, dataDisk := range vm.Properties.StorageProfile.DataDisks { + if dataDisk.ManagedDisk.ID == disk.ID { + vmAttachedToDisk = true + break + } + } + + if !vmAttachedToDisk { + vm.Properties.StorageProfile.DataDisks = []*armcompute.DataDisk{ + { + CreateOption: to.Ptr(armcompute.DiskCreateOptionTypesAttach), + Lun: to.Ptr[int32](0), + ManagedDisk: &armcompute.ManagedDiskParameters{ + ID: disk.ID, + }, + Name: disk.Name, + }, + } + + _, err := s.VMClient.BeginCreateOrUpdate(ctx, s.ScannerResourceGroup, *vm.Name, vm, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "attaching disk %s to VM %s", *disk.Name, *vm.Name) + return err + } + } + + diskResp, err := s.DisksClient.Get(ctx, s.ScannerResourceGroup, *disk.Name, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "getting disk %s", *disk.Name) + return err + } + + if *diskResp.Disk.Properties.DiskState != armcompute.DiskStateAttached { + return provider.RetryableErrorf(VMDiskAttachEstimateTime, "volume is not yet attached, disk is in state: %v", *diskResp.Disk.Properties.DiskState) + } + + return nil +} diff --git a/provider/v2/azure/scanner/snapshot.go b/provider/v2/azure/scanner/snapshot.go new file mode 100644 index 000000000..470cacbf7 --- /dev/null +++ b/provider/v2/azure/scanner/snapshot.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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. + +// nolint:wrapcheck +package scanner + +import ( + "context" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + + "github.com/openclarity/vmclarity/core/to" + "github.com/openclarity/vmclarity/provider" + "github.com/openclarity/vmclarity/provider/v2/azure/utils" +) + +var ( + SnapshotCreateEstimateProvisionTime = 2 * time.Minute + SnapshotDeleteEstimateTime = 2 * time.Minute +) + +func snapshotNameFromJobConfig(config *provider.ScanJobConfig) string { + return "snapshot-" + config.AssetScanID +} + +func (s *Scanner) ensureSnapshotForVMRootVolume(ctx context.Context, config *provider.ScanJobConfig, vm armcompute.VirtualMachine) (armcompute.Snapshot, error) { + snapshotName := snapshotNameFromJobConfig(config) + + snapshotRes, err := s.SnapshotsClient.Get(ctx, s.ScannerResourceGroup, snapshotName, nil) + if err == nil { + if *snapshotRes.Properties.ProvisioningState != provisioningStateSucceeded { + return snapshotRes.Snapshot, provider.RetryableErrorf(SnapshotCreateEstimateProvisionTime, "snapshot is not ready yet") + } + + // Everything is good, the snapshot exists and is provisioned successfully + return snapshotRes.Snapshot, nil + } + + notFound, err := utils.HandleAzureRequestError(err, "getting snapshot %s", snapshotName) + if !notFound { + return armcompute.Snapshot{}, err + } + + _, err = s.SnapshotsClient.BeginCreateOrUpdate(ctx, s.ScannerResourceGroup, snapshotName, armcompute.Snapshot{ + Location: vm.Location, + Properties: &armcompute.SnapshotProperties{ + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID, + }, + }, + }, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "creating snapshot %s", snapshotName) + return armcompute.Snapshot{}, err + } + + return armcompute.Snapshot{}, provider.RetryableErrorf(SnapshotCreateEstimateProvisionTime, "snapshot creating") +} + +func (s *Scanner) ensureSnapshotDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + snapshotName := snapshotNameFromJobConfig(config) + + // nolint:wrapcheck + return utils.EnsureDeleted( + "snapshot", + func() error { + _, err := s.SnapshotsClient.Get(ctx, s.ScannerResourceGroup, snapshotName, nil) + return err + }, + func() error { + _, err := s.SnapshotsClient.BeginDelete(ctx, s.ScannerResourceGroup, snapshotName, nil) + return err + }, + SnapshotDeleteEstimateTime, + ) +} diff --git a/provider/v2/azure/scanner/targetDisk.go b/provider/v2/azure/scanner/targetDisk.go new file mode 100644 index 000000000..409b1399a --- /dev/null +++ b/provider/v2/azure/scanner/targetDisk.go @@ -0,0 +1,134 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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. + +// nolint: wrapcheck +package scanner + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + + "github.com/openclarity/vmclarity/core/to" + "github.com/openclarity/vmclarity/provider" + "github.com/openclarity/vmclarity/provider/v2/azure/utils" +) + +var ( + DiskEstimateProvisionTime = 2 * time.Minute + DiskDeleteEstimateTime = 2 * time.Minute +) + +func volumeNameFromJobConfig(config *provider.ScanJobConfig) string { + return "targetvolume-" + config.AssetScanID +} + +func (s *Scanner) ensureManagedDiskFromSnapshot(ctx context.Context, config *provider.ScanJobConfig, snapshot armcompute.Snapshot) (armcompute.Disk, error) { + volumeName := volumeNameFromJobConfig(config) + + volumeRes, err := s.DisksClient.Get(ctx, s.ScannerResourceGroup, volumeName, nil) + if err == nil { + if *volumeRes.Disk.Properties.ProvisioningState != provisioningStateSucceeded { + return volumeRes.Disk, provider.RetryableErrorf(DiskEstimateProvisionTime, "volume is not ready yet, provisioning state: %s", *volumeRes.Disk.Properties.ProvisioningState) + } + + return volumeRes.Disk, nil + } + + notFound, err := utils.HandleAzureRequestError(err, "getting volume %s", volumeName) + if !notFound { + return armcompute.Disk{}, err + } + + _, err = s.DisksClient.BeginCreateOrUpdate(ctx, s.ScannerResourceGroup, volumeName, armcompute.Disk{ + Location: to.Ptr(s.ScannerLocation), + SKU: &armcompute.DiskSKU{ + Name: to.Ptr(armcompute.DiskStorageAccountTypesStandardSSDLRS), + }, + Properties: &armcompute.DiskProperties{ + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: snapshot.ID, + }, + }, + }, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "creating disk %s", volumeName) + return armcompute.Disk{}, err + } + + return armcompute.Disk{}, provider.RetryableErrorf(DiskEstimateProvisionTime, "disk creating") +} + +func (s *Scanner) ensureManagedDiskFromSnapshotInDifferentRegion(ctx context.Context, config *provider.ScanJobConfig, snapshot armcompute.Snapshot) (armcompute.Disk, error) { + blobURL, err := s.ensureBlobFromSnapshot(ctx, config, snapshot) + if err != nil { + return armcompute.Disk{}, fmt.Errorf("failed to ensure blob from snapshot: %w", err) + } + + volumeName := volumeNameFromJobConfig(config) + + volumeRes, err := s.DisksClient.Get(ctx, s.ScannerResourceGroup, volumeName, nil) + if err == nil { + if *volumeRes.Disk.Properties.ProvisioningState != provisioningStateSucceeded { + return volumeRes.Disk, provider.RetryableErrorf(DiskEstimateProvisionTime, "volume is not ready yet, provisioning state: %s", *volumeRes.Disk.Properties.ProvisioningState) + } + + return volumeRes.Disk, nil + } + + notFound, err := utils.HandleAzureRequestError(err, "getting volume %s", volumeName) + if !notFound { + return armcompute.Disk{}, err + } + + _, err = s.DisksClient.BeginCreateOrUpdate(ctx, s.ScannerResourceGroup, volumeName, armcompute.Disk{ + Location: to.Ptr(s.ScannerLocation), + SKU: &armcompute.DiskSKU{ + Name: to.Ptr(armcompute.DiskStorageAccountTypesStandardSSDLRS), + }, + Properties: &armcompute.DiskProperties{ + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionImport), + SourceURI: to.Ptr(blobURL), + StorageAccountID: to.Ptr(fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s", s.SubscriptionID, s.ScannerResourceGroup, s.ScannerStorageAccountName)), + }, + }, + }, nil) + if err != nil { + _, err := utils.HandleAzureRequestError(err, "creating disk %s", volumeName) + return armcompute.Disk{}, err + } + return armcompute.Disk{}, provider.RetryableErrorf(DiskEstimateProvisionTime, "disk creating") +} + +func (s *Scanner) ensureTargetDiskDeleted(ctx context.Context, config *provider.ScanJobConfig) error { + volumeName := volumeNameFromJobConfig(config) + + return utils.EnsureDeleted( + "target disk", + func() error { + _, err := s.DisksClient.Get(ctx, s.ScannerResourceGroup, volumeName, nil) + return err + }, + func() error { + _, err := s.DisksClient.BeginDelete(ctx, s.ScannerResourceGroup, volumeName, nil) + return err + }, + DiskDeleteEstimateTime, + ) +} diff --git a/provider/v2/azure/utils/utils.go b/provider/v2/azure/utils/utils.go new file mode 100644 index 000000000..86cec8b3b --- /dev/null +++ b/provider/v2/azure/utils/utils.go @@ -0,0 +1,73 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 utils + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + + "github.com/openclarity/vmclarity/provider" +) + +func HandleAzureRequestError(err error, actionTmpl string, parts ...interface{}) (bool, error) { + action := fmt.Sprintf(actionTmpl, parts...) + + var respError *azcore.ResponseError + if !errors.As(err, &respError) { + // Error should be an azcore.ResponseError otherwise something + // bad has happened in the client. + return false, provider.FatalErrorf("unexpected error from azure while %s: %w", action, err) + } + + sc := respError.StatusCode + switch { + case sc >= 400 && sc < 500: + // Client errors (BadRequest/Unauthorized/etc.) are Fatal. We + // also return true to indicate we have NotFound which is a + // special case in a lot of processing. + return sc == http.StatusNotFound, provider.FatalErrorf("error from azure while %s: %w", action, err) + default: + // Everything else is a normal error which can be + // logged as a failure and then the reconciler will try + // again on the next loop. + return false, fmt.Errorf("error from azure while %s: %w", action, err) + } +} + +func EnsureDeleted(resourceType string, getFunc func() error, deleteFunc func() error, estimateTime time.Duration) error { + err := getFunc() + if err != nil { + notFound, err := HandleAzureRequestError(err, "getting %s", resourceType) + // NotFound means that the resource has been deleted + // successfully, all other errors are raised. + if notFound { + return nil + } + return err + } + + err = deleteFunc() + if err != nil { + _, err := HandleAzureRequestError(err, "deleting %s", resourceType) + return err + } + + return provider.RetryableErrorf(estimateTime, "%s delete issued", resourceType) +}