Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

refactor: Azure provider v2 #1351

Merged
merged 9 commits into from
Mar 1, 2024
140 changes: 140 additions & 0 deletions provider/v2/azure/config.go
Original file line number Diff line number Diff line change
@@ -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
}
137 changes: 132 additions & 5 deletions provider/v2/azure/discoverer/discoverer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
ramizpolic marked this conversation as resolved.
Show resolved Hide resolved
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
}
72 changes: 72 additions & 0 deletions provider/v2/azure/discoverer/discoverer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
5 changes: 1 addition & 4 deletions provider/v2/azure/estimator/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Loading
Loading