This commit is contained in:
Martin Strobel 2018-05-29 10:27:32 -07:00
Родитель e37aeca9aa
Коммит fc083137d5
4 изменённых файлов: 359 добавлений и 11 удалений

21
cmd/deployment.go Normal file
Просмотреть файл

@ -0,0 +1,21 @@
package cmd
// DeploymentParameters enables easy marshaling of ARM Template deployment parameters.
type DeploymentParameters struct {
Schema string `json:"$schema"`
ContentVersion string `json:"contentVersion"`
Parameters map[string]DeploymentParameter `json:"parameters"`
}
// DeploymentParameter is an individual entry in the parameter list.
type DeploymentParameter struct {
Value interface{} `json:"value,omitempty"`
}
// NewDeploymentParameters creates a new instance of DeploymentParameters with reasonable defaults but no parameters.
func NewDeploymentParameters() *DeploymentParameters {
return &DeploymentParameters{
Schema: "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#",
ContentVersion: "1.0.0.0",
}
}

Просмотреть файл

@ -23,8 +23,10 @@ package cmd
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
@ -127,9 +129,13 @@ const (
// TemplateShorthand is the abbreviated means of using TemplateName.
TemplateShorthand = "t"
// TemplateDefault
TemplateDefault = "https://invalidtemplate.gobuffalo.io"
templateUsage = "The Azure Resource Management template used to "
// TemplateDefault is the name of the Template to use if no value was provided.
TemplateDefault = "./azuredeploy.json"
// TemplateDefaultLink defines the link that will be used if no local rm-template is found, and a link wasn't
// provided.
TemplateDefaultLink = "https://aka.ms/buffalo-template"
templateUsage = "The Azure Resource Management template which specifies the resources to provision."
)
// These constants define a parameter that Azure subscription to own the resources created.
@ -175,11 +181,15 @@ const (
tenantUsage = "The ID (in form of a UUID) of the organization that the identity being used belongs to. "
)
// These constants define a parameter which forces this program to ignore any ambient Azure settings available as
// environment variables, and instead forces us to use Device Auth instead.
const (
DeviceAuthName = "use-device-auth"
deviceAuthUsage = "Ignore --client-id and --client-secret, interactively authenticate instead."
)
// These constants define a parameter which toggles whether or not status information will be printed as this program
// executes.
const (
VerboseName = "verbose"
VerboseShortname = "v"
@ -187,6 +197,7 @@ const (
)
var status *log.Logger
var errLog *log.Logger
// provisionCmd represents the provision command
var provisionCmd = &cobra.Command{
@ -203,17 +214,19 @@ var provisionCmd = &cobra.Command{
subscriptionID := provisionConfig.GetString(SubscriptionName)
clientID := provisionConfig.GetString(ClientIDName)
clientSecret := provisionConfig.GetString(ClientSecretName)
status.Print("subscription selected: ", subscriptionID)
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Minute)
defer cancel()
auth, err := getAuthorizer(ctx, subscriptionID, clientID, clientSecret, provisionConfig.GetString(TenantIDName))
if err != nil {
fmt.Fprintln(os.Stderr, "unable to authenticate: ", err)
errLog.Print("unable to authenticate: ", err)
return
}
status.Print("tenant selected: ", provisionConfig.GetString(TenantIDName))
status.Print("subscription selected: ", subscriptionID)
templateLocation := provisionConfig.GetString(TemplateName)
status.Println("template selected: ", templateLocation)
groups := resources.NewGroupsClient(subscriptionID)
groups.Authorizer = auth
@ -228,7 +241,7 @@ var provisionCmd = &cobra.Command{
rgName := provisionConfig.GetString(ResoureGroupName)
created, err := insertResourceGroup(ctx, groups, rgName, provisionConfig.GetString(LocationName))
if err != nil {
fmt.Fprintf(os.Stderr, "unable to fetch or create resource group %s: %v\n", rgName, err)
errLog.Printf("unable to fetch or create resource group %s: %v\n", rgName, err)
return
}
if created {
@ -241,7 +254,36 @@ var provisionCmd = &cobra.Command{
deployments := resources.NewDeploymentsClient(subscriptionID)
deployments.Authorizer = auth
status.Print("Done.")
status.Println("starting deployment")
params := NewDeploymentParameters()
params.Parameters["database"] = DeploymentParameter{provisionConfig.GetString(DatabaseName)}
params.Parameters["imageName"] = DeploymentParameter{provisionConfig.GetString(ImageName)}
template, err := getDeploymentTemplate(templateLocation)
if err != nil {
errLog.Print("unable to fetch template: ", err)
return
}
template.Parameters = params
template.Mode = resources.Incremental
fut, err := deployments.CreateOrUpdate(ctx, rgName, "buffalo-app", resources.Deployment{
Properties: template,
})
if err != nil {
errLog.Print("unable to start deployment: ", err)
return
}
err = fut.WaitForCompletion(ctx, deployments.Client)
if err != nil {
errLog.Print("unable to poll for completion progress, your assets may or may not have finished provisioning")
return
}
status.Print("finished deployment")
exitStatus = 0
},
Args: func(cmd *cobra.Command, args []string) error {
@ -253,7 +295,7 @@ var provisionCmd = &cobra.Command{
hasClientSecret := provisionConfig.GetString(ClientSecretName) != ""
if (hasClientID || hasClientSecret) && !(hasClientID && hasClientSecret) {
return errors.New("--client-id and --client-secret must be speficied together or not at all")
return errors.New("--client-id and --client-secret must be specified together or not at all")
}
var err error
@ -266,7 +308,7 @@ var provisionCmd = &cobra.Command{
if provisionConfig.GetBool(VerboseName) {
statusWriter = os.Stdout
}
status = log.New(statusWriter, "[INFO] ", 0)
status = newFormattedLog(statusWriter, "information")
return nil
},
@ -342,6 +384,7 @@ func getAuthorizer(ctx context.Context, subscriptionID, clientID, clientSecret,
return nil, err
}
status.Println("service principal token created for client: ", clientID)
t := auth.Token()
intermediate = &t
}
@ -360,7 +403,31 @@ func getAuthorizer(ctx context.Context, subscriptionID, clientID, clientSecret,
}
func getDatabaseFlavor(buffaloRoot string) string {
return "postgres" // TODO: parse buffalo app for the database they're using.
return "postgres" // TODO (#29): parse buffalo app for the database they're using.
}
func getDeploymentTemplate(raw string) (*resources.DeploymentProperties, error) {
if isSupportedLink(raw) {
return &resources.DeploymentProperties{
TemplateLink: &resources.TemplateLink{
URI: &raw,
},
}, nil
}
handle, err := os.Open(raw)
if err != nil {
return nil, err
}
contents, err := ioutil.ReadAll(handle)
if err != nil {
return nil, err
}
return &resources.DeploymentProperties{
Template: json.RawMessage(contents),
}, nil
}
func getTenant(ctx context.Context, common *adal.Token, subscription string) (string, autorest.Authorizer, error) {
@ -423,7 +490,17 @@ func isSupportedLink(subject string) bool {
return ok
}
func newFormattedLog(output io.Writer, identifier string) *log.Logger {
const identLen = 4
for len(identifier) < identLen {
identifier = identifier + " "
}
return log.New(output, fmt.Sprintf("[%s] ", strings.ToUpper(identifier)[:identLen]), log.Ldate|log.Ltime)
}
func init() {
errLog = newFormattedLog(os.Stderr, "error")
azureCmd.AddCommand(provisionCmd)
// Here you will define your flags and configuration settings.
@ -454,7 +531,7 @@ func init() {
provisionConfig.SetDefault(EnvironmentName, EnvironmentDefault)
provisionConfig.SetDefault(DatabaseName, getDatabaseFlavor("."))
provisionConfig.SetDefault(ResoureGroupName, "buffalo-app") // TODO: generate a random suffix
provisionConfig.SetDefault(ResoureGroupName, "buffalo-app") // TODO (#30): generate a random suffix
provisionConfig.SetDefault(LocationName, LocationDefault)
provisionCmd.Flags().StringP(ImageName, ImageShorthand, ImageDefault, imageUsage)

135
cmd/provision_test.go Normal file
Просмотреть файл

@ -0,0 +1,135 @@
package cmd
import (
"testing"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
)
func Test_getDeploymentTemplate_links(t *testing.T) {
testCases := []string{
"https://aka.ms/buffalo-template",
"http://aka.ms/buffalo-template",
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
result, err := getDeploymentTemplate(tc)
if err != nil {
t.Error(err)
}
if result.Template != nil {
t.Log("unexpected value present in template")
t.Fail()
}
if result.TemplateLink == nil {
t.Log("unexpected nil template link")
t.Fail()
return
}
if result.TemplateLink.URI == nil {
t.Log("unexpected nil uri")
t.Fail()
return
}
if got := *result.TemplateLink.URI; !strings.EqualFold(got, tc) {
t.Logf("got:\n\t%q\nwant:\n\t%q", got, tc)
t.Fail()
return
}
})
}
}
func Test_getDeploymentTemplate_localFiles(t *testing.T) {
testCases := []string{
"./testdata/template1.json",
}
for _, tc := range testCases {
t.Run(tc, func(t *testing.T) {
handle, err := os.Open(tc)
if err != nil {
t.Error(err)
return
}
result, err := getDeploymentTemplate(tc)
if err != nil {
t.Error(err)
return
}
if result.TemplateLink != nil {
t.Log("unexpected value present in template link")
t.Fail()
}
if result.Template == nil {
t.Log("unexpected nil template")
t.Fail()
return
}
want, err := ioutil.ReadAll(handle)
if err != nil {
t.Error(err)
}
minimized := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(minimized)
err = enc.Encode(json.RawMessage(want))
if err != nil {
t.Error(err)
return
}
want = minimized.Bytes()
want = []byte(strings.TrimSpace(string(want)))
got, err := json.Marshal(result.Template)
report := func(got, want []byte) string {
shrink := func(target []byte, maxLength int) (retval []byte) {
if len(target) > maxLength {
retval = append(target[:maxLength/2], []byte("...")...)
retval = append(retval, target[len(target)-maxLength/2:]...)
} else {
retval = target
}
return
}
const maxLength = 30
gotLength := len(got)
got = shrink(got, maxLength)
wantLength := len(want)
want = shrink(want, maxLength)
return fmt.Sprintf("\ngot (len %d):\n\t%q\nwant (len %d):\n\t%q", gotLength, got, wantLength, want)
}
if len(want) == len(got) {
for i, current := range want {
if got[i] != current {
t.Log(report(got, want))
t.Fail()
break
}
}
} else {
t.Log(report(got, want))
t.Fail()
}
})
}
}

115
cmd/testdata/template1.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,115 @@
{
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"imageName" : {
"type": "String",
"defaultValue": "appsvc/sample-hello-world:latest"
},
"name": {
"type": "String",
"defaultValue": "[concat('site', uniqueString(resourceGroup().id, deployment().name))]"
},
"database": {
"type": "String",
"defaultValue": "none",
"allowedValues": [
"none",
"postgres"
]
},
"databaseName": {
"type": "String",
"defaultValue": "buffalo_development"
},
"databaseAdministratorLogin": {
"type": "String",
"defaultValue": "[concat('admin', parameters('name'))]"
},
"databaseAdministratorLoginPassword": {
"type": "SecureString",
"defaultValue": ""
}
},
"variables": {
"hostingPlanName": "[concat('hostingPlan-', parameters('name'))]",
"postgresName": "[concat(parameters('name'), '-postgres')]",
"postgresConnection": "[concat('postgres://', parameters('databaseAdministratorLogin'), ':', parameters('databaseAdministratorLoginPassword'), '@', variables('postgresname'), '.postgres.database.azure.com/', parameters('databaseName'), '?sslmode=required')]"
},
"resources": [
{
"type": "Microsoft.Web/sites",
"name": "[parameters('name')]",
"apiVersion": "2016-03-01",
"location": "[resourceGroup().location]",
"tags": {
"[concat('hidden-related:', subscription().id, '/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanName'))]": "empty",
"gobuffalo": "empty"
},
"properties": {
"name": "[parameters('name')]",
"siteConfig": {
"appSettings": [
{
"name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
"value": "false"
}
],
"connectionStrings":[
{
"name":"DATABASE_URL",
"connectionString": "[if(equals(parameters('database'), 'postgres'), variables('postgresConnection'), 'not applicable')]",
"type":"custom"
}
],
"appCommandLine": "",
"linuxFxVersion": "[concat('DOCKER|', parameters('imageName'))]"
},
"serverFarmId": "[concat(subscription().id, '/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanName'))]",
"hostingEnvironment": ""
},
"dependsOn": [
"[variables('hostingPlanName')]",
"[variables('postgresName')]"
]
},
{
"type": "Microsoft.Web/serverfarms",
"sku": {
"Tier": "Basic",
"Name": "B1"
},
"kind": "linux",
"name": "[variables('hostingPlanName')]",
"apiVersion": "2016-09-01",
"location": "[resourceGroup().location]",
"properties": {
"name": "[variables('hostingPlanName')]",
"workerSizeId": "0",
"reserved": true,
"numberOfWorkers": "1",
"hostingEnvironment": ""
}
},
{
"condition":"[equals(parameters('database'), 'postgres')]",
"type": "Microsoft.DBforPostgreSQL/servers",
"sku": {
"name": "B_Gen5_1",
"family": "Gen5",
"capacity": "",
"size": "5120",
"tier":"Basic"
},
"kind":"",
"name":"[variables('postgresName')]",
"apiVersion": "2017-12-01-preview",
"location":"[resourceGroup().location]",
"properties": {
"version": "9.6",
"administratorLogin": "[parameters('databaseAdministratorLogin')]",
"administratorLoginPassword": "[parameters('databaseAdministratorLoginPassword')]"
}
}
]
}