Skip to content

Commit 61113e8

Browse files
authored
Merge pull request #12 from Azure/pr-xml-regions
Support promotion to more than two regions
2 parents e1c20bd + 472fdc5 commit 61113e8

File tree

55 files changed

+2236
-162
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2236
-162
lines changed

Godeps/Godeps.json

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,22 @@ USAGE:
5555
azure-extensions-cli [global options] command [command options] [arguments...]
5656
5757
COMMANDS:
58-
new-extension-manifest Creates an XML file used to publish or update extension.
59-
new-extension Creates a new type of extension, not for releasing new versions.
60-
new-extension-version Publishes a new type of extension internally.
61-
promote-single-region Promote published internal extension to a PROD Location.
62-
promote-two-regions Promote published extension to two PROD Locations.
63-
promote-to-prod Promote published extension to all PROD Locations.
64-
list-versions Lists all published extension versions for subscription
58+
new-extension-manifest Creates an XML file used to publish or update extension.
59+
new-extension Creates a new type of extension, not for releasing new versions.
60+
new-extension-version Publishes a new type of extension internally.
61+
promote Promote published internal extension to one or more PROD Locations.
62+
promote-all-regions Promote published extension to all PROD Locations.
63+
list-versions Lists all published extension versions for subscription
6564
replication-status Retrieves replication status for an uploaded extension package
6665
unpublish-version Marks the specified version of the extension internal. Does not delete.
67-
delete-version Deletes the extension version. It should be unpublished first.
68-
help, h Shows a list of commands or help for one command
66+
delete-version Deletes the extension version. It should be unpublished first.
67+
help, h Shows a list of commands or help for one command
6968
7069
GLOBAL OPTIONS:
7170
--help, -h show help
7271
--version, -v print the version
7372
```
7473

75-
7674
## Installing (or building from source)
7775

7876
You can head over to the **Releases** section to download a binary built for various platforms.
@@ -81,10 +79,36 @@ If you need to compile from the source code, make sure you have Go compiler 1.6+
8179
Check out the project, set the GOPATH environment variable correctly (if necessary) and
8280
run `go build`. This should compile a binary.
8381

84-
## Author
82+
## Overview
83+
84+
The CLI makes it easy (easier) to publish an Azure extension. An example workflow is provided below. This workflow
85+
assumes an extension type already exists, which is why the command **new-extension-version** is used. (If the type does
86+
not exist use substitute for the command new-extension instead.)
87+
88+
Not all command line parameters are shown for each command, only the salient options are shown.
89+
90+
Step 1 - create an extension manifest.
91+
92+
1. ./azure-extensions-cli new-extension-manifest
93+
94+
Step 2 - publish an extension internally.
95+
96+
1. ./azure-extensions-cli new-extension-version
97+
98+
Step 3 - rollout the extension to Azure, by slowly including more and more regions. It is recommended that you pause
99+
24 hours between regions.
85100

86-
Ahmet Alp Balkan
101+
> Every time a new region is added, the previous regions must be included with the promote command.
102+
103+
1. ./azure-extensions-cli promote --region "West Central US"
104+
1. ./azure-extensions-cli promote --region "West Central US" --region "North US"
105+
1. ./azure-extensions-cli promote --region "West Central US" --region "North US" --region "West US"
106+
1. ./azure-extensions-cli promote ...
107+
108+
Step 4 - promote the extension to all Azure regions.
87109

110+
1. ./azure-extensions-cli promote-all-regions
111+
88112
## TODO
89113

90114
- [ ] make `replication-status` exit with appropriate code if replication is not completed.

main.go

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,10 @@ var (
6161
flStorageAccount = cli.StringFlag{
6262
Name: "storage-account",
6363
Usage: "Name of an existing storage account to be used in uploading the extension package temporarily."}
64-
flRegion1 = cli.StringFlag{
65-
Name: "region-1",
66-
Usage: "Primary pilot location to roll out the extension (e.g. 'Japan East')",
67-
EnvVar: "REGION1"}
68-
flRegion2 = cli.StringFlag{
69-
Name: "region-2",
70-
Usage: "Primary pilot location to roll out the extension (e.g. 'Brazil South')",
71-
EnvVar: "REGION2"}
64+
flRegion = cli.StringSliceFlag{
65+
Name: "region",
66+
Usage: "List of one or more regions to rollout an extension (e.g. 'Japan East')",
67+
}
7268
flJSON = cli.BoolFlag{
7369
Name: "json",
7470
Usage: "Print output as JSON"}
@@ -120,21 +116,17 @@ func main() {
120116
Usage: "Publishes a new type of extension internally.",
121117
Flags: []cli.Flag{flMgtURL, flSubsID, flSubsCert, flManifest},
122118
Action: updateExtension},
123-
{Name: "promote-single-region",
124-
Usage: "Promote published internal extension to PROD in a Location.",
125-
Flags: []cli.Flag{flMgtURL, flSubsID, flSubsCert, flManifest, flRegion1},
126-
Action: promoteToFirstSlice},
127-
{Name: "promote-two-regions",
128-
Usage: "Promote published extension to PROD in two Locations.",
129-
Flags: []cli.Flag{flMgtURL, flSubsID, flSubsCert, flManifest, flRegion1, flRegion2},
130-
Action: promoteToSecondSlice},
119+
{Name: "promote",
120+
Usage: "Promote published internal extension to PROD in one or more locations.",
121+
Flags: []cli.Flag{flMgtURL, flSubsID, flSubsCert, flManifest, flRegion},
122+
Action: promoteToRegions},
131123
{Name: "promote-all-regions",
132124
Usage: "Promote published extension to all Locations.",
133125
Flags: []cli.Flag{flMgtURL, flSubsID, flSubsCert, flManifest},
134126
Action: promoteToAllRegions},
135127
{Name: "list-versions",
136128
Usage: "Lists all published extension versions for subscription",
137-
Flags: []cli.Flag{flMgtURL, flSubsID, flSubsCert},
129+
Flags: []cli.Flag{flMgtURL, flSubsID, flSubsCert, flJSON},
138130
Action: listVersions},
139131
{Name: "replication-status",
140132
Usage: "Retrieves replication status for an uploaded extension package",

manifest.go

Lines changed: 152 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,175 @@
11
package main
22

33
import (
4-
"os"
5-
"text/template"
4+
"encoding/xml"
5+
"fmt"
66

77
log "github.com/Sirupsen/logrus"
88
"github.com/codegangsta/cli"
9+
"io/ioutil"
10+
"strings"
911
)
1012

13+
type certificate struct {
14+
StoreLocation string `xml:"StoreLocation,omitempty"`
15+
StoreName string `xml:"StoreName,omitempty"`
16+
ThumbprintRequired bool `xml:"ThumbprintRequired,omitempty"`
17+
ThumbprintAlgorithm string `xml:"ThumbprintAlgorithm,omitempty"`
18+
}
19+
20+
// NOTE(@boumenot): there is probably a better way to express this. If
21+
// you know please share...
22+
//
23+
// The only difference between ExtensionImage and ExtensionImageGlobal is the
24+
// Regions element. This element can be in three different states to my
25+
// knowledge.
26+
//
27+
// 1. not defined
28+
// 2. <Regions>Region1;Region2</Regions>
29+
// 3. <Regions></Regions>
30+
//
31+
// Case (1) occurs when an extension is first published. Case(2) occurs when
32+
// an extension is promoted to one or two regions. Case (3) occurs when an
33+
// extension is published to all regions.
34+
//
35+
// I do not know how to express all three cases using Go's XML serializer.
36+
//
37+
type extensionImage struct {
38+
XMLName string `xml:"ExtensionImage"`
39+
NS string `xml:"xmlns,attr"`
40+
ProviderNameSpace string `xml:"ProviderNameSpace"`
41+
Type string `xml:"Type"`
42+
Version string `xml:"Version"`
43+
Label string `xml:"Label"`
44+
HostingResources string `xml:"HostingResources"`
45+
MediaLink string `xml:"MediaLink"`
46+
Certificate *certificate `xml:"Certificate,omitempty"`
47+
PublicConfigurationSchema string `xml:"PublicConfigurationSchema,omitempty"`
48+
PrivateConfigurationSchema string `xml:"PrivateConfigurationSchema,omitempty"`
49+
Description string `xml:"Description"`
50+
BlockRoleUponFailure string `xml:"BlockRoleUponFailure,omitempty"`
51+
IsInternalExtension bool `xml:"IsInternalExtension"`
52+
Eula string `xml:"Eula,omitempty"`
53+
PrivacyURI string `xml:"PrivacyUri,omitempty"`
54+
HomepageURI string `xml:"HomepageUri,omitempty"`
55+
IsJSONExtension bool `xml:"IsJsonExtension,omitempty"`
56+
CompanyName string `xml:"CompanyName,omitempty"`
57+
SupportedOS string `xml:"SupportedOS,omitempty"`
58+
Regions string `xml:"Regions,omitempty"`
59+
}
60+
61+
type extensionImageGlobal struct {
62+
XMLName string `xml:"ExtensionImage"`
63+
NS string `xml:"xmlns,attr"`
64+
ProviderNameSpace string `xml:"ProviderNameSpace"`
65+
Type string `xml:"Type"`
66+
Version string `xml:"Version"`
67+
Label string `xml:"Label"`
68+
HostingResources string `xml:"HostingResources"`
69+
MediaLink string `xml:"MediaLink"`
70+
Certificate *certificate `xml:"Certificate,omitempty"`
71+
PublicConfigurationSchema string `xml:"PublicConfigurationSchema,omitempty"`
72+
PrivateConfigurationSchema string `xml:"PrivateConfigurationSchema,omitempty"`
73+
Description string `xml:"Description"`
74+
BlockRoleUponFailure string `xml:"BlockRoleUponFailure,omitempty"`
75+
IsInternalExtension bool `xml:"IsInternalExtension"`
76+
Eula string `xml:"Eula,omitempty"`
77+
PrivacyURI string `xml:"PrivacyUri,omitempty"`
78+
HomepageURI string `xml:"HomepageUri,omitempty"`
79+
IsJSONExtension bool `xml:"IsJsonExtension,omitempty"`
80+
CompanyName string `xml:"CompanyName,omitempty"`
81+
SupportedOS string `xml:"SupportedOS,omitempty"`
82+
Regions string `xml:"Regions"`
83+
}
84+
85+
type extensionManifest interface {
86+
Marshal() ([]byte, error)
87+
}
88+
89+
func isGuestAgent(providerNameSpace string) bool {
90+
return "Microsoft.OSTCLinuxAgent" == providerNameSpace
91+
}
92+
93+
func newExtensionImageManifest(filename string, regions []string) (extensionManifest, error) {
94+
b, err := ioutil.ReadFile(filename)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
var manifest extensionImage
100+
err = xml.Unmarshal(b, &manifest)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
manifest.Regions = strings.Join(regions, ";")
106+
107+
if !isGuestAgent(manifest.ProviderNameSpace) {
108+
manifest.IsInternalExtension = false
109+
} else {
110+
log.Debug("VM agent namespace detected, IsInternalExtension ignored")
111+
}
112+
113+
return &manifest, nil
114+
}
115+
116+
func newExtensionImageGlobalManifest(filename string) (extensionManifest, error) {
117+
b, err := ioutil.ReadFile(filename)
118+
if err != nil {
119+
return nil, err
120+
}
121+
122+
var manifest extensionImageGlobal
123+
err = xml.Unmarshal(b, &manifest)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
manifest.IsInternalExtension = !isGuestAgent(manifest.ProviderNameSpace)
129+
return &manifest, nil
130+
}
131+
132+
func (ext *extensionImage) Marshal() ([]byte, error) {
133+
return xml.Marshal(*ext)
134+
}
135+
136+
func (ext *extensionImageGlobal) Marshal() ([]byte, error) {
137+
return xml.Marshal(*ext)
138+
}
139+
11140
func newExtensionManifest(c *cli.Context) {
12141
cl := mkClient(checkFlag(c, flMgtURL.Name), checkFlag(c, flSubsID.Name), checkFlag(c, flSubsCert.Name))
13142
storageRealm := checkFlag(c, flStorageRealm.Name)
14143
storageAccount := checkFlag(c, flStorageAccount.Name)
15144
extensionPkg := checkFlag(c, flPackage.Name)
16145

17-
var p struct {
18-
Namespace, Name, Version, BlobURL, Label, Description, Eula, Privacy, Homepage, Company, OS string
19-
}
20-
flags := []struct {
21-
ref *string
22-
fl string
23-
}{
24-
{&p.Namespace, flNamespace.Name},
25-
{&p.Name, flName.Name},
26-
{&p.Version, flVersion.Name},
27-
{&p.Label, "label"},
28-
{&p.Description, "description"},
29-
{&p.Eula, "eula-url"},
30-
{&p.Privacy, "privacy-url"},
31-
{&p.Homepage, "homepage-url"},
32-
{&p.Company, "company"},
33-
{&p.OS, "supported-os"},
34-
}
35-
for _, f := range flags {
36-
*f.ref = checkFlag(c, f.fl)
37-
}
38-
39146
// Upload extension blob
40147
blobURL, err := uploadBlob(cl, storageRealm, storageAccount, extensionPkg)
41148
if err != nil {
42149
log.Fatal(err)
43150
}
44151
log.Debugf("Extension package uploaded to: %s", blobURL)
45-
p.BlobURL = blobURL
46-
47-
// doing a text template is easier and let us create comments (xml encoder can't)
48-
// that are used as placeholders later on.
49-
manifestXML := `<?xml version="1.0" encoding="utf-8" ?>
50-
<ExtensionImage xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
51-
<!-- WARNING: Ordering of fields matter in this file. -->
52-
<ProviderNameSpace>{{.Namespace}}</ProviderNameSpace>
53-
<Type>{{.Name}}</Type>
54-
<Version>{{.Version}}</Version>
55-
<Label>{{.Label}}</Label>
56-
<HostingResources>VmRole</HostingResources>
57-
<MediaLink>{{.BlobURL}}</MediaLink>
58-
<Description>{{.Description}}</Description>
59-
<IsInternalExtension>true</IsInternalExtension>
60-
<Eula>{{.Eula}}</Eula>
61-
<PrivacyUri>{{.Privacy}}</PrivacyUri>
62-
<HomepageUri>{{.Homepage}}</HomepageUri>
63-
<IsJsonExtension>true</IsJsonExtension>
64-
<CompanyName>{{.Company}}</CompanyName>
65-
<SupportedOS>{{.OS}}</SupportedOS>
66-
<!--%REGIONS%-->
67-
</ExtensionImage>
68-
`
69-
tpl, err := template.New("manifest").Parse(manifestXML)
70-
if err != nil {
71-
log.Fatalf("template parse error: %v", err)
152+
153+
manifest := extensionImage{
154+
ProviderNameSpace: checkFlag(c, flNamespace.Name),
155+
Type: checkFlag(c, flName.Name),
156+
Version: checkFlag(c, flVersion.Name),
157+
Label: "label",
158+
Description: "description",
159+
IsInternalExtension: true,
160+
MediaLink: blobURL,
161+
Eula: "eula-url",
162+
PrivacyURI: "privacy-url",
163+
HomepageURI: "homepage-url",
164+
IsJSONExtension: true,
165+
CompanyName: "company",
166+
SupportedOS: "supported-os",
72167
}
73-
if err = tpl.Execute(os.Stdout, p); err != nil {
74-
log.Fatalf("template execute error: %v", err)
168+
169+
bs, err := xml.MarshalIndent(manifest, "", " ")
170+
if err != nil {
171+
log.Fatalf("xml marshall error: %v", err)
75172
}
173+
174+
fmt.Println(string(bs))
76175
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<ExtensionImage xmlns="http://schemas.microsoft.com/windowsazure">
2+
<ProviderNameSpace>Microsoft.OSCTExtensions</ProviderNameSpace>
3+
<Type>CustomScriptForLinux</Type>
4+
<Version>4.3.2.1</Version>
5+
<Label>Microsoft Azure Custom Script Extension for Linux Virtual Machines</Label>
6+
<HostingResources>VmRole</HostingResources>
7+
<MediaLink>http://localhost/extension.zip</MediaLink>
8+
<Description>Please consider using Microsoft.Azure.Extensions.CustomScript instead.</Description>
9+
<IsInternalExtension>true</IsInternalExtension>
10+
<Eula>https://github.com/Azure/azure-linux-extensions/blob/master/LICENSE-2_0.txt</Eula>
11+
<PrivacyUri>http://www.microsoft.com/privacystatement/en-us/OnlineServices/Default.aspx</PrivacyUri>
12+
<HomepageUri>https://github.com/Azure/azure-linux-extensions</HomepageUri>
13+
<IsJsonExtension>true</IsJsonExtension>
14+
<CompanyName>Microsoft</CompanyName>
15+
<SupportedOS>Linux</SupportedOS>
16+
<Regions>South Central US</Regions>
17+
</ExtensionImage>

0 commit comments

Comments
 (0)