Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure identity support #456

Merged
merged 5 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions auth/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,45 @@ func (ap *AzureProfile) AsOptions() []Options {
return o
}

type SubFilter struct {
name string
id string
}

func FilterByName(name string) SubFilter {
return SubFilter{name: name}
}
func FilterByID(id string) SubFilter {
return SubFilter{id: id}
}
func (s *SubFilter) IsEmpty() bool {
return s.name == "" && s.id == ""
}
func (s *SubFilter) Matches(opts *Options) bool {
if s.name != "" && opts.SubscriptionName == s.name {
return true
}
if s.id != "" && opts.SubscriptionID == s.id {
return true
}
return false
}

// SubscriptionOptions returns the name subscription in the Azure profile as a Options struct.
// If the subscription name is "", the first subscription is returned.
// If there are no subscriptions or the named subscription is not found, SubscriptionOptions returns nil.
func (ap *AzureProfile) SubscriptionOptions(name string) *Options {
func (ap *AzureProfile) SubscriptionOptions(filter SubFilter) *Options {
opts := ap.AsOptions()

if len(opts) == 0 {
return nil
}

if name == "" {
if filter.IsEmpty() {
return &opts[0]
} else {
for _, o := range ap.AsOptions() {
if o.SubscriptionName == name {
if filter.Matches(&o) {
return &o
}
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/kola/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ func init() {
sv(&kola.AzureOptions.BlobURL, "azure-blob-url", "", "Azure source page blob to be copied from a public/SAS URL, recommended way (from \"plume pre-release\" or \"ore azure upload-blob-arm\")")
sv(&kola.AzureOptions.ImageFile, "azure-image-file", "", "Azure image file (local image to upload in the temporary kola resource group)")
sv(&kola.AzureOptions.DiskURI, "azure-disk-uri", "", "Azure disk uri (custom images)")
sv(&kola.AzureOptions.Publisher, "azure-publisher", "CoreOS", "Azure image publisher (default \"CoreOS\"")
sv(&kola.AzureOptions.Offer, "azure-offer", "CoreOS", "Azure image offer (default \"CoreOS\"")
sv(&kola.AzureOptions.Publisher, "azure-publisher", "kinvolk", "Azure image publisher (default \"kinvolk\"")
sv(&kola.AzureOptions.Offer, "azure-offer", "flatcar-container-linux-free", "Azure image offer (default \"flatcar-container-linux-free\"")
sv(&kola.AzureOptions.Sku, "azure-sku", "alpha", "Azure image sku/channel (default \"alpha\"")
sv(&kola.AzureOptions.Version, "azure-version", "", "Azure image version")
sv(&kola.AzureOptions.Location, "azure-location", "westus", "Azure location (default \"westus\"")
Expand All @@ -122,6 +122,7 @@ func init() {
sv(&kola.AzureOptions.VnetSubnetName, "azure-vnet-subnet-name", "", "Use a pre-existing virtual network for created instances. Specify as vnet-name/subnet-name. If subnet name is omitted then \"default\" is assumed")
bv(&kola.AzureOptions.UseGallery, "azure-use-gallery", false, "Use gallery image instead of managed image")
bv(&kola.AzureOptions.UsePrivateIPs, "azure-use-private-ips", false, "Assume nodes are reachable using private IP addresses")
bv(&kola.AzureOptions.UseIdentity, "azure-identity", false, "Use VM managed identity for authentication (default false)")

// do-specific options
sv(&kola.DOOptions.ConfigPath, "do-config-file", "", "DigitalOcean config file (default \"~/"+auth.DOConfigPath+"\")")
Expand Down
4 changes: 4 additions & 0 deletions cmd/ore/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var (
azureAuth string
azureSubscription string
azureLocation string
useIdentity bool

api *azure.API
)
Expand All @@ -43,10 +44,12 @@ func init() {
cli.WrapPreRun(Azure, preauth)

sv := Azure.PersistentFlags().StringVar
bv := Azure.PersistentFlags().BoolVar
sv(&azureProfile, "azure-profile", "", "Azure Profile json file")
sv(&azureAuth, "azure-auth", "", "Azure auth location (default \"~/"+auth.AzureAuthPath+"\")")
sv(&azureSubscription, "azure-subscription", "", "Azure subscription name. If unset, the first is used.")
sv(&azureLocation, "azure-location", "westus", "Azure location (default \"westus\")")
bv(&useIdentity, "azure-identity", false, "Use VM managed identity for authentication (default false)")
}

func preauth(cmd *cobra.Command, args []string) error {
Expand All @@ -57,6 +60,7 @@ func preauth(cmd *cobra.Command, args []string) error {
AzureAuthLocation: azureAuth,
AzureSubscription: azureSubscription,
Location: azureLocation,
UseIdentity: useIdentity,
})
if err != nil {
plog.Fatalf("Failed to create Azure API: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.19
require (
cloud.google.com/go/storage v1.27.0
github.com/Azure/azure-sdk-for-go v56.2.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.19
github.com/Azure/go-autorest/autorest/azure/auth v0.5.8
github.com/Microsoft/azure-vhd-utils v0.0.0-20210818134022-97083698b75f
github.com/aws/aws-sdk-go v1.44.46
Expand Down Expand Up @@ -54,7 +55,6 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.8.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.19 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
Expand Down
151 changes: 113 additions & 38 deletions platform/api/azure/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package azure

import (
"context"
"fmt"
"math/rand"
"os"
Expand All @@ -27,8 +28,10 @@ import (
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-03-01/compute"
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-10-01/resources"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2021-01-01/subscriptions"
armStorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-01-01/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/coreos/pkg/capnslog"

Expand Down Expand Up @@ -58,42 +61,38 @@ type Network struct {
subnet network.Subnet
}

// New creates a new Azure client. If no publish settings file is provided or
// can't be parsed, an anonymous client is created.
func New(opts *Options) (*API, error) {
conf := management.DefaultConfig()
conf.APIVersion = "2015-04-01"

if opts.ManagementURL != "" {
conf.ManagementURL = opts.ManagementURL
}

if opts.StorageEndpointSuffix == "" {
opts.StorageEndpointSuffix = storage.DefaultBaseURL
}

func setOptsFromProfile(opts *Options) error {
profiles, err := internalAuth.ReadAzureProfile(opts.AzureProfile)
if err != nil {
return nil, fmt.Errorf("couldn't read Azure profile: %v", err)
}

subOpts := profiles.SubscriptionOptions(opts.AzureSubscription)
if subOpts == nil {
return nil, fmt.Errorf("Azure subscription named %q doesn't exist in %q", opts.AzureSubscription, opts.AzureProfile)
return fmt.Errorf("couldn't read Azure profile: %v", err)
}

if os.Getenv("AZURE_AUTH_LOCATION") == "" {
if opts.AzureAuthLocation == "" {
user, err := user.Current()
if err != nil {
return nil, err
return err
}
opts.AzureAuthLocation = filepath.Join(user.HomeDir, internalAuth.AzureAuthPath)
}
// TODO: Move to Flight once built to allow proper unsetting
os.Setenv("AZURE_AUTH_LOCATION", opts.AzureAuthLocation)
}

var subOpts *internalAuth.Options
if opts.AzureSubscription == "" {
settings, err := auth.GetSettingsFromFile()
if err != nil {
return err
}
subOpts = profiles.SubscriptionOptions(internalAuth.FilterByID(settings.GetSubscriptionID()))
} else {
subOpts = profiles.SubscriptionOptions(internalAuth.FilterByName(opts.AzureSubscription))
}
if subOpts == nil {
return fmt.Errorf("Azure subscription named %q doesn't exist in %q", opts.AzureSubscription, opts.AzureProfile)
}

if opts.SubscriptionID == "" {
opts.SubscriptionID = subOpts.SubscriptionID
}
Expand All @@ -114,6 +113,37 @@ func New(opts *Options) (*API, error) {
opts.StorageEndpointSuffix = subOpts.StorageEndpointSuffix
}

return nil
}

// New creates a new Azure client. If no publish settings file is provided or
// can't be parsed, an anonymous client is created.
func New(opts *Options) (*API, error) {
var err error
conf := management.DefaultConfig()
conf.APIVersion = "2015-04-01"

if opts.ManagementURL != "" {
conf.ManagementURL = opts.ManagementURL
}

if opts.StorageEndpointSuffix == "" {
opts.StorageEndpointSuffix = storage.DefaultBaseURL
}

if !opts.UseIdentity {
err = setOptsFromProfile(opts)
if err != nil {
return nil, fmt.Errorf("failed to get options from azure profile: %w", err)
}
} else {
subid, err := msiGetSubscriptionID()
if err != nil {
return nil, fmt.Errorf("failed to query subscription id: %w", err)
}
opts.SubscriptionID = subid
}

var client management.Client
if opts.ManagementCertificate != nil {
client, err = management.NewClientFromConfig(opts.SubscriptionID, opts.ManagementCertificate, conf)
Expand All @@ -137,50 +167,95 @@ func New(opts *Options) (*API, error) {
return api, nil
}

func (a *API) SetupClients() error {
auther, err := auth.NewAuthorizerFromFile(resources.DefaultBaseURI)
func (a *API) newAuthorizer(baseURI string) (autorest.Authorizer, error) {
if !a.Opts.UseIdentity {
return auth.NewAuthorizerFromFile(baseURI)
}
settings, err := auth.GetSettingsFromEnvironment()
if err != nil {
return err
return nil, err
}
settings, err := auth.GetSettingsFromFile()
return settings.GetMSI().Authorizer()
}

func msiGetSubscriptionID() (string, error) {
settings, err := auth.GetSettingsFromEnvironment()
if err != nil {
return "", err
}
subid := settings.GetSubscriptionID()
if subid != "" {
return subid, nil
}
auther, err := settings.GetMSI().Authorizer()
if err != nil {
return "", err
}
client := subscriptions.NewClient()
client.Authorizer = auther
iter, err := client.ListComplete(context.Background())
if err != nil {
return "", fmt.Errorf("failed to list subscriptions: %w", err)
}
for sub := iter.Value(); iter.NotDone(); iter.Next() {
// this should never happen
if sub.SubscriptionID == nil {
continue
}
if subid != "" {
return "", fmt.Errorf("multiple subscriptions found; pass one explicitly using the %s environment variable", auth.SubscriptionID)
}
subid = *sub.SubscriptionID
}
if subid == "" {
return "", fmt.Errorf("no subscriptions found; pass one explicitly using the %s environment variable", auth.SubscriptionID)
}
plog.Infof("Using subscription %s", subid)
return subid, nil
}

func (a *API) SetupClients() error {
auther, err := a.newAuthorizer(resources.DefaultBaseURI)
if err != nil {
return err
}
a.rgClient = resources.NewGroupsClient(settings.GetSubscriptionID())
subid := a.Opts.SubscriptionID

a.rgClient = resources.NewGroupsClient(subid)
a.rgClient.Authorizer = auther

a.depClient = resources.NewDeploymentsClient(settings.GetSubscriptionID())
a.depClient = resources.NewDeploymentsClient(subid)
a.depClient.Authorizer = auther

auther, err = auth.NewAuthorizerFromFile(compute.DefaultBaseURI)
auther, err = a.newAuthorizer(compute.DefaultBaseURI)
if err != nil {
return err
}
a.imgClient = compute.NewImagesClient(settings.GetSubscriptionID())
a.imgClient = compute.NewImagesClient(subid)
a.imgClient.Authorizer = auther
a.compClient = compute.NewVirtualMachinesClient(settings.GetSubscriptionID())
a.compClient = compute.NewVirtualMachinesClient(subid)
a.compClient.Authorizer = auther
a.vmImgClient = compute.NewVirtualMachineImagesClient(settings.GetSubscriptionID())
a.vmImgClient = compute.NewVirtualMachineImagesClient(subid)
a.vmImgClient.Authorizer = auther

auther, err = auth.NewAuthorizerFromFile(network.DefaultBaseURI)
auther, err = a.newAuthorizer(network.DefaultBaseURI)
if err != nil {
return err
}
a.netClient = network.NewVirtualNetworksClient(settings.GetSubscriptionID())
a.netClient = network.NewVirtualNetworksClient(subid)
a.netClient.Authorizer = auther
a.subClient = network.NewSubnetsClient(settings.GetSubscriptionID())
a.subClient = network.NewSubnetsClient(subid)
a.subClient.Authorizer = auther
a.ipClient = network.NewPublicIPAddressesClient(settings.GetSubscriptionID())
a.ipClient = network.NewPublicIPAddressesClient(subid)
a.ipClient.Authorizer = auther
a.intClient = network.NewInterfacesClient(settings.GetSubscriptionID())
a.intClient = network.NewInterfacesClient(subid)
a.intClient.Authorizer = auther

auther, err = auth.NewAuthorizerFromFile(armStorage.DefaultBaseURI)
auther, err = a.newAuthorizer(armStorage.DefaultBaseURI)
if err != nil {
return err
}
a.accClient = armStorage.NewAccountsClient(settings.GetSubscriptionID())
a.accClient = armStorage.NewAccountsClient(subid)
a.accClient.Authorizer = auther

return nil
Expand Down
11 changes: 11 additions & 0 deletions platform/api/azure/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ func (a *API) getVMParameters(name, userdata, sshkey, storageAccountURI string,
Sku: &a.Opts.Sku,
Version: &a.Opts.Version,
}
if a.Opts.Version == "latest" {
var top int32 = 1
list, err := a.vmImgClient.List(context.TODO(), a.Opts.Location, a.Opts.Publisher, a.Opts.Offer, a.Opts.Sku, "", &top, "name desc")
if err != nil {
plog.Warningf("failed to get image list: %v; continuing", err)
} else if list.Value == nil || len(*list.Value) == 0 || (*list.Value)[0].Name == nil {
plog.Warningf("no images found; continuing")
} else {
a.Opts.Version = *(*list.Value)[0].Name
}
}
// lookup plan information for image
imgInfo, err := a.vmImgClient.Get(context.TODO(), a.Opts.Location, *imgRef.Publisher, *imgRef.Offer, *imgRef.Sku, *imgRef.Version)
if err == nil && imgInfo.Plan != nil {
Expand Down
1 change: 1 addition & 0 deletions platform/api/azure/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Options struct {
HyperVGeneration string
VnetSubnetName string
UseGallery bool
UseIdentity bool
UsePrivateIPs bool

SubscriptionName string
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading