Skip to content
This repository has been archived by the owner on May 7, 2021. It is now read-only.

platform/azure: add kola/ore commands for Azure #771

Merged
merged 11 commits into from Jul 11, 2019
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.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
37 changes: 35 additions & 2 deletions README.md
Expand Up @@ -199,7 +199,7 @@ Plume release handles this as well, so it does not need to be run as part of
the release process.

## Platform Credentials
Each platform reads the credentials it uses from different files. The `aws`, `do`, `esx` and `packet`
Each platform reads the credentials it uses from different files. The `aws`, `azure`, `do`, `esx` and `packet`
platforms support selecting from multiple configured credentials, call "profiles". The examples below
are for the "default" profile, but other profiles can be specified in the credentials files and selected
via the `--<platform-name>-profile` flag:
Expand Down Expand Up @@ -231,7 +231,40 @@ sudo emerge --ask awscli
```

### azure
TBD (FIXME)
`azure` uses `~/.azure/azureProfile.json`. This can be created using the `az` [command](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli):
```
$ az login`
```
It also requires that the environment variable `AZURE_AUTH_LOCATION` points to a JSON file (this can also be set via the `--azure-auth` parameter). The JSON file will require a service provider active directory account to be created.

Service provider accounts can be created via the `az` command (the output will contain an `appId` field which is used as the `clientId` variable in the `AZURE_AUTH_LOCATION` JSON):
```
az ad sp create-for-rbac
```

The client secret can be created inside of the Azure portal when looking at the service provider account under the `Azure Active Directory` service on the `App registrations` tab.

You can find your subscriptionId & tenantId in the `~/.azure/azureProfile.json` via:
```
cat ~/.azure/azureProfile.json | jq '{subscriptionId: .subscriptions[].id, tenantId: .subscriptions[].tenantId}'
```

The JSON file exported to the variable `AZURE_AUTH_LOCATION` should be generated by hand and have the following contents:
```
{
"clientId": "<service provider id>",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should note this is from the az ad sp create-for-rbac

"clientSecret": "<service provider secret>",
"subscriptionId": "<subscription id>",
"tenantId": "<tenant id>",
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
"resourceManagerEndpointUrl": "https://management.azure.com/",
"activeDirectoryGraphResourceId": "https://graph.windows.net/",
"sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
"galleryEndpointUrl": "https://gallery.azure.com/",
"managementEndpointUrl": "https://management.core.windows.net/"
}

```

### do
`do` uses `~/.config/digitalocean.json`. This can be configured manually:
Expand Down
57 changes: 46 additions & 11 deletions auth/azure.go
Expand Up @@ -18,14 +18,39 @@ import (
"bytes"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit: s/form/from/ in commit message.

"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"

"github.com/coreos/mantle/platform/api/azure"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"

"github.com/coreos/mantle/platform"
)

const (
AzureAuthPath = ".azure/credentials.json"
AzureProfilePath = ".azure/azureProfile.json"
)

const AzureProfilePath = ".azure/azureProfile.json"
// A version of the Options struct from platform/api/azure that only
// contains the ASM values. Otherwise there's a cyclical depdendence
// because platform/api/azure has to import auth to have access to
// the ReadAzureProfile function.
type Options struct {
ajeddeloh marked this conversation as resolved.
Show resolved Hide resolved
*platform.Options

SubscriptionName string
SubscriptionID string

// Azure API endpoint. If unset, the Azure SDK default will be used.
ManagementURL string
ManagementCertificate []byte

// Azure Storage API endpoint suffix. If unset, the Azure SDK default will be used.
StorageEndpointSuffix string
}

type AzureEnvironment struct {
ActiveDirectoryEndpointURL string `json:"activeDirectoryEndpointUrl"`
Expand Down Expand Up @@ -68,13 +93,13 @@ type AzureProfile struct {
Subscriptions []AzureSubscription `json:"subscriptions"`
}

// AsOptions converts all subscriptions into a slice of azure.Options.
// AsOptions converts all subscriptions into a slice of Options.
// If there is an environment with a name matching the subscription, that environment's storage endpoint will be copied to the options.
func (ap *AzureProfile) AsOptions() []azure.Options {
var o []azure.Options
func (ap *AzureProfile) AsOptions() []Options {
var o []Options

for _, sub := range ap.Subscriptions {
newo := azure.Options{
newo := Options{
SubscriptionName: sub.Name,
SubscriptionID: sub.ID,
ManagementURL: sub.ManagementEndpointURL,
Expand All @@ -95,10 +120,10 @@ func (ap *AzureProfile) AsOptions() []azure.Options {
return o
}

// SubscriptionOptions returns the name subscription in the Azure profile as a azure.Options struct.
// 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) *azure.Options {
func (ap *AzureProfile) SubscriptionOptions(name string) *Options {
opts := ap.AsOptions()

if len(opts) == 0 {
Expand Down Expand Up @@ -131,14 +156,13 @@ func ReadAzureProfile(path string) (*AzureProfile, error) {
path = filepath.Join(user.HomeDir, AzureProfilePath)
}

f, err := os.Open(path)
contents, err := DecodeBOMFile(path)
if err != nil {
return nil, err
}
defer f.Close()

var ap AzureProfile
if err := json.NewDecoder(f).Decode(&ap); err != nil {
if err := json.Unmarshal(contents, &ap); err != nil {
return nil, err
}

Expand All @@ -148,3 +172,14 @@ func ReadAzureProfile(path string) (*AzureProfile, error) {

return &ap, nil
}

func DecodeBOMFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
decoder := unicode.UTF8.NewDecoder()
reader := transform.NewReader(f, unicode.BOMOverride(decoder))
return ioutil.ReadAll(reader)
}
19 changes: 19 additions & 0 deletions cmd/kola/kola.go
Expand Up @@ -139,6 +139,15 @@ func writeProps() error {
AMI string `json:"ami"`
InstanceType string `json:"type"`
}
type Azure struct {
DiskURI string `json:"diskUri"`
Publisher string `json:"publisher"`
Offer string `json:"offer"`
Sku string `json:"sku"`
Version string `json:"version"`
Location string `json:"location"`
Size string `json:"size"`
}
type DO struct {
Region string `json:"region"`
Size string `json:"size"`
Expand Down Expand Up @@ -175,6 +184,7 @@ func writeProps() error {
Board string `json:"board"`
OSContainer string `json:"oscontainer"`
AWS AWS `json:"aws"`
Azure Azure `json:"azure"`
DO DO `json:"do"`
ESX ESX `json:"esx"`
GCE GCE `json:"gce"`
Expand All @@ -193,6 +203,15 @@ func writeProps() error {
AMI: kola.AWSOptions.AMI,
InstanceType: kola.AWSOptions.InstanceType,
},
Azure: Azure{
DiskURI: kola.AzureOptions.DiskURI,
Publisher: kola.AzureOptions.Publisher,
Offer: kola.AzureOptions.Offer,
Sku: kola.AzureOptions.Sku,
Version: kola.AzureOptions.Version,
Location: kola.AzureOptions.Location,
Size: kola.AzureOptions.Size,
},
DO: DO{
Region: kola.DOOptions.Region,
Size: kola.DOOptions.Size,
Expand Down
13 changes: 12 additions & 1 deletion cmd/kola/options.go
Expand Up @@ -30,7 +30,7 @@ var (
kolaPlatform string
defaultTargetBoard = sdk.DefaultBoard()
kolaArchitectures = []string{"amd64"}
kolaPlatforms = []string{"aws", "do", "esx", "gce", "openstack", "packet", "qemu", "qemu-unpriv"}
kolaPlatforms = []string{"aws", "azure", "do", "esx", "gce", "openstack", "packet", "qemu", "qemu-unpriv"}
kolaDistros = []string{"cl", "fcos", "rhcos"}
kolaDefaultImages = map[string]string{
"amd64-usr": sdk.BuildRoot() + "/images/amd64-usr/latest/coreos_production_image.bin",
Expand Down Expand Up @@ -81,6 +81,17 @@ func init() {
sv(&kola.AWSOptions.SecurityGroup, "aws-sg", "kola", "AWS security group name")
sv(&kola.AWSOptions.IAMInstanceProfile, "aws-iam-profile", "kola", "AWS IAM instance profile name")

// azure-specific options
sv(&kola.AzureOptions.AzureProfile, "azure-profile", "", "Azure profile (default \"~/"+auth.AzureProfilePath+"\")")
sv(&kola.AzureOptions.AzureAuthLocation, "azure-auth", "", "Azure auth location (default \"~/"+auth.AzureAuthPath+"\")")
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.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\"")
sv(&kola.AzureOptions.Size, "azure-size", "Standard_D2_v2", "Azure machine size (default \"Standard_D2_v2\")")

// do-specific options
sv(&kola.DOOptions.ConfigPath, "do-config-file", "", "DigitalOcean config file (default \"~/"+auth.DOConfigPath+"\")")
sv(&kola.DOOptions.Profile, "do-profile", "", "DigitalOcean profile (default \"default\")")
Expand Down
21 changes: 10 additions & 11 deletions cmd/ore/azure/azure.go
Expand Up @@ -32,7 +32,9 @@ var (
}

azureProfile string
azureAuth string
azureSubscription string
azureLocation string

api *azure.API
)
Expand All @@ -42,23 +44,20 @@ func init() {

sv := Azure.PersistentFlags().StringVar
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\")")
}

func preauth(cmd *cobra.Command, args []string) error {
plog.Printf("Creating Azure API...")

prof, err := auth.ReadAzureProfile(azureProfile)
if err != nil {
plog.Fatalf("Failed to read Azure Profile %q: %v", azureProfile, err)
}

opt := prof.SubscriptionOptions(azureSubscription)
if opt == nil {
plog.Fatalf("Azure subscription named %q doesn't exist in %q", azureSubscription, azureProfile)
}

a, err := azure.New(opt)
a, err := azure.New(&azure.Options{
AzureProfile: azureProfile,
AzureAuthLocation: azureAuth,
AzureSubscription: azureSubscription,
Location: azureLocation,
})
if err != nil {
plog.Fatalf("Failed to create Azure API: %v", err)
}
Expand Down
74 changes: 74 additions & 0 deletions cmd/ore/azure/create-image-arm.go
@@ -0,0 +1,74 @@
// Copyright 2018 CoreOS, Inc.
//
// 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/json"
"fmt"
"os"

"github.com/spf13/cobra"
)

var (
cmdCreateImageARM = &cobra.Command{
Use: "create-image-arm",
Short: "Create Azure image",
Long: "Create Azure image from a blob url",
RunE: runCreateImageARM,
}

imageName string
blobUrl string
resourceGroup string
)

func init() {
sv := cmdCreateImageARM.Flags().StringVar

sv(&imageName, "image-name", "", "image name")
sv(&blobUrl, "image-blob", "", "source blob url")
sv(&resourceGroup, "resource-group", "kola", "resource group name")

Azure.AddCommand(cmdCreateImageARM)
}

func runCreateImageARM(cmd *cobra.Command, args []string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was really confused for a sec and thought Azure supported ARM.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah ARM (Azure Resource Manager) can be a confusing name when looking from our usual scope.

if err := api.SetupClients(); err != nil {
fmt.Fprintf(os.Stderr, "setting up clients: %v\n", err)
os.Exit(1)
}
img, err := api.CreateImage(imageName, resourceGroup, blobUrl)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't create image: %v\n", err)
os.Exit(1)
}
if img.ID == nil {
fmt.Fprintf(os.Stderr, "received nil image\n")
os.Exit(1)
}
err = json.NewEncoder(os.Stdout).Encode(&struct {
ID *string
Location *string
}{
ID: img.ID,
Location: img.Location,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't encode result: %v\n", err)
os.Exit(1)
}
return nil
}
52 changes: 52 additions & 0 deletions cmd/ore/azure/gc.go
@@ -0,0 +1,52 @@
// Copyright 2018 CoreOS, Inc.
//
// 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 (
"fmt"
"os"
"time"

"github.com/spf13/cobra"
)

var (
cmdGC = &cobra.Command{
Use: "gc",
Short: "GC resources in Azure",
Long: `Delete instances created over the given duration ago`,
RunE: runGC,
}

gcDuration time.Duration
)

func init() {
Azure.AddCommand(cmdGC)
cmdGC.Flags().DurationVar(&gcDuration, "duration", 5*time.Hour, "how old resources must be before they're considered garbage")
}

func runGC(cmd *cobra.Command, args []string) error {
if err := api.SetupClients(); err != nil {
fmt.Fprintf(os.Stderr, "setting up clients: %v\n", err)
os.Exit(1)
}
err := api.GC(gcDuration)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't gc: %v\n", err)
os.Exit(1)
}
return nil
}