Repository: tetratelabs/getenvoy
Pull request #6 (2019-07-04 09:06:27)

Add manifest libraries
Signed-off-by: Liam White <liam@tetrate.io>


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1e91298..93beb49 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -21,11 +21,19 @@ jobs:
       - image: circleci/golang:1.12.6
     steps:
       - checkout
-      - run: wget -c https://github.com/liamawhite/licenser/releases/download/v0.4.0/licenser_0.4.0_Linux_x86_64.tar.gz -O - | sudo tar -xz -C /usr/bin
+      - run: ./ci/install
       - run: ./ci/lint
+  test:
+    resource_class: small
+    docker:
+      - image: circleci/golang:1.12.6
+    steps:
+      - checkout
+      - run: ./ci/test
 
 workflows:
   version: 2
   all:
     jobs:
       - lint
+      - test
diff --git a/api/manifest.pb.go b/api/manifest.pb.go
index 02dc23d..4850aba 100644
--- a/api/manifest.pb.go
+++ b/api/manifest.pb.go
@@ -56,17 +56,17 @@ func (Compliance) EnumDescriptor() ([]byte, []int) {
 	return fileDescriptor_346685e380de5b1f, []int{0}
 }
 
-type OperatingSystem int32
+type OperatingSystemName int32
 
 const (
-	OperatingSystem_UBUNTU OperatingSystem = 0
-	OperatingSystem_DEBIAN OperatingSystem = 1
-	OperatingSystem_CENTOS OperatingSystem = 2
-	OperatingSystem_RHEL   OperatingSystem = 3
-	OperatingSystem_MACOS  OperatingSystem = 4
+	OperatingSystemName_UBUNTU OperatingSystemName = 0
+	OperatingSystemName_DEBIAN OperatingSystemName = 1
+	OperatingSystemName_CENTOS OperatingSystemName = 2
+	OperatingSystemName_RHEL   OperatingSystemName = 3
+	OperatingSystemName_MACOS  OperatingSystemName = 4
 )
 
-var OperatingSystem_name = map[int32]string{
+var OperatingSystemName_name = map[int32]string{
 	0: "UBUNTU",
 	1: "DEBIAN",
 	2: "CENTOS",
@@ -74,7 +74,7 @@ var OperatingSystem_name = map[int32]string{
 	4: "MACOS",
 }
 
-var OperatingSystem_value = map[string]int32{
+var OperatingSystemName_value = map[string]int32{
 	"UBUNTU": 0,
 	"DEBIAN": 1,
 	"CENTOS": 2,
@@ -82,21 +82,21 @@ var OperatingSystem_value = map[string]int32{
 	"MACOS":  4,
 }
 
-func (x OperatingSystem) String() string {
-	return proto.EnumName(OperatingSystem_name, int32(x))
+func (x OperatingSystemName) String() string {
+	return proto.EnumName(OperatingSystemName_name, int32(x))
 }
 
-func (OperatingSystem) EnumDescriptor() ([]byte, []int) {
+func (OperatingSystemName) EnumDescriptor() ([]byte, []int) {
 	return fileDescriptor_346685e380de5b1f, []int{1}
 }
 
 // Builds must be uniquely addressable from the top level so that they can be used to look up the location of binaries.
-// Format: filter_profile(-compliance_profile)/operating_system-operating_system_version:envoy_version
+// Format: filter_profile(-compliance_profile):envoy_version/operating_system-operating_system_version
 // Examples:
-//   - istio-fips1402/ubuntu-16.04:1.10.1
-//   - standard/debian-8:nightly
+//   - istio-fips1402:1.10.1/ubuntu-16.04
+//   - standard:nightly/debian-8
 type Manifest struct {
-	Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
+	ManifestVersion string `protobuf:"bytes,1,opt,name=manifest_version,json=manifestVersion,proto3" json:"manifest_version,omitempty"`
 	// Key is composite key of the value's filter_profile and compliance_profile
 	// Note: compliance_profile is optional
 	// Format: filter_profile(-compliance_profile)
@@ -131,9 +131,9 @@ func (m *Manifest) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_Manifest proto.InternalMessageInfo
 
-func (m *Manifest) GetVersion() string {
+func (m *Manifest) GetManifestVersion() string {
 	if m != nil {
-		return m.Version
+		return m.ManifestVersion
 	}
 	return ""
 }
@@ -146,18 +146,19 @@ func (m *Manifest) GetFlavors() map[string]*Flavor {
 }
 
 type Flavor struct {
+	// This is duplicated in order to make flavor easier to sort
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
 	// Filter profile is the name of the collection of filters
-	// E.g. Standard, Minimal, Istio
-	FilterProfile string `protobuf:"bytes,1,opt,name=filter_profile,json=filterProfile,proto3" json:"filter_profile,omitempty"`
+	// E.g. standard, minimal, istio
+	FilterProfile string `protobuf:"bytes,2,opt,name=filter_profile,json=filterProfile,proto3" json:"filter_profile,omitempty"`
 	// All filters available in this flavor
-	Filters     []string     `protobuf:"bytes,2,rep,name=filters,proto3" json:"filters,omitempty"`
-	Compliances []Compliance `protobuf:"varint,3,rep,packed,name=compliances,proto3,enum=api.Compliance" json:"compliances,omitempty"`
-	// Key is composite key of the value's operating_system, operating_system_version and envoy_version
-	// Format: operating_system-operating_system_version:envoy_version
-	Builds               map[string]*Build `protobuf:"bytes,5,rep,name=builds,proto3" json:"builds,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
-	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
-	XXX_unrecognized     []byte            `json:"-"`
-	XXX_sizecache        int32             `json:"-"`
+	Filters     []string     `protobuf:"bytes,3,rep,name=filters,proto3" json:"filters,omitempty"`
+	Compliances []Compliance `protobuf:"varint,4,rep,packed,name=compliances,proto3,enum=api.Compliance" json:"compliances,omitempty"`
+	// Key is Envoy version
+	Versions             map[string]*Version `protobuf:"bytes,5,rep,name=versions,proto3" json:"versions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	XXX_NoUnkeyedLiteral struct{}            `json:"-"`
+	XXX_unrecognized     []byte              `json:"-"`
+	XXX_sizecache        int32               `json:"-"`
 }
 
 func (m *Flavor) Reset()         { *m = Flavor{} }
@@ -185,6 +186,13 @@ func (m *Flavor) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_Flavor proto.InternalMessageInfo
 
+func (m *Flavor) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
 func (m *Flavor) GetFilterProfile() string {
 	if m != nil {
 		return m.FilterProfile
@@ -206,7 +214,105 @@ func (m *Flavor) GetCompliances() []Compliance {
 	return nil
 }
 
-func (m *Flavor) GetBuilds() map[string]*Build {
+func (m *Flavor) GetVersions() map[string]*Version {
+	if m != nil {
+		return m.Versions
+	}
+	return nil
+}
+
+type Version struct {
+	// This duplicated in order to make version easier to sort
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// Key is operating system
+	// Note: Keys in proto maps cannot be enums, so this has to be a string.
+	OperatingSystems     map[string]*OperatingSystem `protobuf:"bytes,2,rep,name=operating_systems,json=operatingSystems,proto3" json:"operating_systems,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	XXX_NoUnkeyedLiteral struct{}                    `json:"-"`
+	XXX_unrecognized     []byte                      `json:"-"`
+	XXX_sizecache        int32                       `json:"-"`
+}
+
+func (m *Version) Reset()         { *m = Version{} }
+func (m *Version) String() string { return proto.CompactTextString(m) }
+func (*Version) ProtoMessage()    {}
+func (*Version) Descriptor() ([]byte, []int) {
+	return fileDescriptor_346685e380de5b1f, []int{2}
+}
+
+func (m *Version) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Version.Unmarshal(m, b)
+}
+func (m *Version) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Version.Marshal(b, m, deterministic)
+}
+func (m *Version) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Version.Merge(m, src)
+}
+func (m *Version) XXX_Size() int {
+	return xxx_messageInfo_Version.Size(m)
+}
+func (m *Version) XXX_DiscardUnknown() {
+	xxx_messageInfo_Version.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Version proto.InternalMessageInfo
+
+func (m *Version) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *Version) GetOperatingSystems() map[string]*OperatingSystem {
+	if m != nil {
+		return m.OperatingSystems
+	}
+	return nil
+}
+
+type OperatingSystem struct {
+	// Matches the key, duplicated here to be explicit.
+	Name                 OperatingSystemName `protobuf:"varint,1,opt,name=name,proto3,enum=api.OperatingSystemName" json:"name,omitempty"`
+	Builds               []*Build            `protobuf:"bytes,2,rep,name=builds,proto3" json:"builds,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}            `json:"-"`
+	XXX_unrecognized     []byte              `json:"-"`
+	XXX_sizecache        int32               `json:"-"`
+}
+
+func (m *OperatingSystem) Reset()         { *m = OperatingSystem{} }
+func (m *OperatingSystem) String() string { return proto.CompactTextString(m) }
+func (*OperatingSystem) ProtoMessage()    {}
+func (*OperatingSystem) Descriptor() ([]byte, []int) {
+	return fileDescriptor_346685e380de5b1f, []int{3}
+}
+
+func (m *OperatingSystem) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_OperatingSystem.Unmarshal(m, b)
+}
+func (m *OperatingSystem) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_OperatingSystem.Marshal(b, m, deterministic)
+}
+func (m *OperatingSystem) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_OperatingSystem.Merge(m, src)
+}
+func (m *OperatingSystem) XXX_Size() int {
+	return xxx_messageInfo_OperatingSystem.Size(m)
+}
+func (m *OperatingSystem) XXX_DiscardUnknown() {
+	xxx_messageInfo_OperatingSystem.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_OperatingSystem proto.InternalMessageInfo
+
+func (m *OperatingSystem) GetName() OperatingSystemName {
+	if m != nil {
+		return m.Name
+	}
+	return OperatingSystemName_UBUNTU
+}
+
+func (m *OperatingSystem) GetBuilds() []*Build {
 	if m != nil {
 		return m.Builds
 	}
@@ -214,20 +320,18 @@ func (m *Flavor) GetBuilds() map[string]*Build {
 }
 
 type Build struct {
-	OperatingSystem        OperatingSystem `protobuf:"varint,1,opt,name=operating_system,json=operatingSystem,proto3,enum=api.OperatingSystem" json:"operating_system,omitempty"`
-	OperatingSystemVersion string          `protobuf:"bytes,2,opt,name=operating_system_version,json=operatingSystemVersion,proto3" json:"operating_system_version,omitempty"`
-	EnvoyVersion           string          `protobuf:"bytes,3,opt,name=envoy_version,json=envoyVersion,proto3" json:"envoy_version,omitempty"`
-	DownloadLocationUrl    string          `protobuf:"bytes,4,opt,name=download_location_url,json=downloadLocationUrl,proto3" json:"download_location_url,omitempty"`
-	XXX_NoUnkeyedLiteral   struct{}        `json:"-"`
-	XXX_unrecognized       []byte          `json:"-"`
-	XXX_sizecache          int32           `json:"-"`
+	OperatingSystemVersion string   `protobuf:"bytes,1,opt,name=operating_system_version,json=operatingSystemVersion,proto3" json:"operating_system_version,omitempty"`
+	DownloadLocationUrl    string   `protobuf:"bytes,2,opt,name=download_location_url,json=downloadLocationUrl,proto3" json:"download_location_url,omitempty"`
+	XXX_NoUnkeyedLiteral   struct{} `json:"-"`
+	XXX_unrecognized       []byte   `json:"-"`
+	XXX_sizecache          int32    `json:"-"`
 }
 
 func (m *Build) Reset()         { *m = Build{} }
 func (m *Build) String() string { return proto.CompactTextString(m) }
 func (*Build) ProtoMessage()    {}
 func (*Build) Descriptor() ([]byte, []int) {
-	return fileDescriptor_346685e380de5b1f, []int{2}
+	return fileDescriptor_346685e380de5b1f, []int{4}
 }
 
 func (m *Build) XXX_Unmarshal(b []byte) error {
@@ -248,13 +352,6 @@ func (m *Build) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_Build proto.InternalMessageInfo
 
-func (m *Build) GetOperatingSystem() OperatingSystem {
-	if m != nil {
-		return m.OperatingSystem
-	}
-	return OperatingSystem_UBUNTU
-}
-
 func (m *Build) GetOperatingSystemVersion() string {
 	if m != nil {
 		return m.OperatingSystemVersion
@@ -262,13 +359,6 @@ func (m *Build) GetOperatingSystemVersion() string {
 	return ""
 }
 
-func (m *Build) GetEnvoyVersion() string {
-	if m != nil {
-		return m.EnvoyVersion
-	}
-	return ""
-}
-
 func (m *Build) GetDownloadLocationUrl() string {
 	if m != nil {
 		return m.DownloadLocationUrl
@@ -278,45 +368,52 @@ func (m *Build) GetDownloadLocationUrl() string {
 
 func init() {
 	proto.RegisterEnum("api.Compliance", Compliance_name, Compliance_value)
-	proto.RegisterEnum("api.OperatingSystem", OperatingSystem_name, OperatingSystem_value)
+	proto.RegisterEnum("api.OperatingSystemName", OperatingSystemName_name, OperatingSystemName_value)
 	proto.RegisterType((*Manifest)(nil), "api.Manifest")
 	proto.RegisterMapType((map[string]*Flavor)(nil), "api.Manifest.FlavorsEntry")
 	proto.RegisterType((*Flavor)(nil), "api.Flavor")
-	proto.RegisterMapType((map[string]*Build)(nil), "api.Flavor.BuildsEntry")
+	proto.RegisterMapType((map[string]*Version)(nil), "api.Flavor.VersionsEntry")
+	proto.RegisterType((*Version)(nil), "api.Version")
+	proto.RegisterMapType((map[string]*OperatingSystem)(nil), "api.Version.OperatingSystemsEntry")
+	proto.RegisterType((*OperatingSystem)(nil), "api.OperatingSystem")
 	proto.RegisterType((*Build)(nil), "api.Build")
 }
 
 func init() { proto.RegisterFile("api/manifest.proto", fileDescriptor_346685e380de5b1f) }
 
 var fileDescriptor_346685e380de5b1f = []byte{
-	// 452 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0xdd, 0x6e, 0xda, 0x30,
-	0x14, 0x6e, 0x12, 0xa0, 0xe5, 0xa4, 0x40, 0xe4, 0xfd, 0x45, 0xec, 0x26, 0xeb, 0x34, 0x09, 0xf5,
-	0x22, 0x5d, 0xb3, 0x5e, 0x54, 0xbb, 0x99, 0x0a, 0x4b, 0xb7, 0x4e, 0x2d, 0x54, 0xa1, 0xec, 0x36,
-	0x72, 0xc1, 0x4c, 0xd6, 0x8c, 0x1d, 0x39, 0x81, 0x89, 0xe7, 0xd9, 0x63, 0xed, 0x35, 0xf6, 0x00,
-	0x53, 0xec, 0xb8, 0x4b, 0x51, 0xef, 0x8e, 0xbf, 0xbf, 0x9c, 0x2f, 0x36, 0x20, 0x9c, 0xd1, 0x93,
-	0x15, 0xe6, 0x74, 0x49, 0xf2, 0x22, 0xcc, 0xa4, 0x28, 0x04, 0x72, 0x70, 0x46, 0x8f, 0x7e, 0x5b,
-	0x70, 0x70, 0x53, 0xe1, 0xc8, 0x87, 0xfd, 0x0d, 0x91, 0x39, 0x15, 0xdc, 0xb7, 0x02, 0x6b, 0xd0,
-	0x4e, 0xcc, 0x11, 0x9d, 0xc1, 0xfe, 0x92, 0xe1, 0x8d, 0x90, 0xb9, 0x6f, 0x07, 0xce, 0xc0, 0x8d,
-	0xfa, 0x21, 0xce, 0x68, 0x68, 0x9c, 0xe1, 0xa5, 0x26, 0x63, 0x5e, 0xc8, 0x6d, 0x62, 0xa4, 0xfd,
-	0x2f, 0x70, 0x58, 0x27, 0x90, 0x07, 0xce, 0x4f, 0xb2, 0xad, 0xb2, 0xcb, 0x11, 0xbd, 0x81, 0xe6,
-	0x06, 0xb3, 0x35, 0xf1, 0xed, 0xc0, 0x1a, 0xb8, 0x91, 0xab, 0x52, 0xb5, 0x27, 0xd1, 0xcc, 0x47,
-	0xfb, 0xdc, 0x3a, 0xfa, 0x6b, 0x41, 0x4b, 0xa3, 0xe8, 0x1d, 0x74, 0x97, 0x94, 0x15, 0x44, 0xa6,
-	0x99, 0x14, 0x4b, 0xca, 0x48, 0x15, 0xd7, 0xd1, 0xe8, 0xad, 0x06, 0xcb, 0x2a, 0x1a, 0xd0, 0x0b,
-	0xb7, 0x13, 0x73, 0x44, 0xa7, 0xe0, 0xce, 0xc5, 0x2a, 0x63, 0x14, 0xf3, 0x39, 0xc9, 0x7d, 0x27,
-	0x70, 0x06, 0xdd, 0xa8, 0xa7, 0x3e, 0x3c, 0x7a, 0xc0, 0x93, 0xba, 0x06, 0x9d, 0x40, 0xeb, 0x7e,
-	0x4d, 0xd9, 0x22, 0xf7, 0x9b, 0xaa, 0xfc, 0xab, 0xda, 0x9a, 0xe1, 0x50, 0x31, 0xba, 0x79, 0x25,
-	0xeb, 0xc7, 0xe0, 0xd6, 0xe0, 0x27, 0x7a, 0x07, 0x8f, 0x7b, 0x83, 0x0a, 0x54, 0x96, 0x7a, 0xed,
-	0x3f, 0x16, 0x34, 0x15, 0x88, 0x3e, 0x81, 0x27, 0x32, 0x22, 0x71, 0x41, 0xf9, 0x8f, 0x34, 0xdf,
-	0xe6, 0x05, 0x59, 0xa9, 0xb8, 0x6e, 0xf4, 0x5c, 0x59, 0x27, 0x86, 0x9c, 0x2a, 0x2e, 0xe9, 0x89,
-	0xc7, 0x00, 0x3a, 0x07, 0x7f, 0x37, 0x20, 0x35, 0x77, 0x6d, 0xab, 0xbd, 0x5e, 0xee, 0x58, 0xbe,
-	0x57, 0x57, 0xff, 0x16, 0x3a, 0x84, 0x6f, 0xc4, 0xf6, 0x41, 0xee, 0x28, 0xf9, 0xa1, 0x02, 0x8d,
-	0x28, 0x82, 0x17, 0x0b, 0xf1, 0x8b, 0x33, 0x81, 0x17, 0x29, 0x13, 0x73, 0x5c, 0x50, 0xc1, 0xd3,
-	0xb5, 0x64, 0x7e, 0x43, 0x89, 0x9f, 0x19, 0xf2, 0xba, 0xe2, 0x66, 0x92, 0x1d, 0xbf, 0x06, 0xf8,
-	0xff, 0xc3, 0x51, 0x07, 0xda, 0x97, 0x57, 0xb7, 0xd3, 0xf4, 0xf4, 0xec, 0x7d, 0xe4, 0xed, 0x1d,
-	0x7f, 0x83, 0xde, 0x4e, 0x27, 0x04, 0xd0, 0x9a, 0x0d, 0x67, 0xe3, 0xbb, 0x99, 0xb7, 0x57, 0xce,
-	0x9f, 0xe3, 0xe1, 0xd5, 0xc5, 0xd8, 0xb3, 0xca, 0x79, 0x14, 0x8f, 0xef, 0x26, 0x53, 0xcf, 0x46,
-	0x07, 0xd0, 0x48, 0xbe, 0xc6, 0xd7, 0x9e, 0x83, 0xda, 0xd0, 0xbc, 0xb9, 0x18, 0x4d, 0xa6, 0x5e,
-	0xe3, 0xbe, 0xa5, 0xde, 0xfb, 0x87, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x58, 0x72, 0xac, 0x50,
-	0x05, 0x03, 0x00, 0x00,
+	// 514 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0xdf, 0x6e, 0xd3, 0x3c,
+	0x1c, 0x5d, 0x9a, 0xfe, 0xfd, 0x75, 0x6d, 0xf3, 0x79, 0xdf, 0x50, 0x28, 0x37, 0x25, 0x12, 0x52,
+	0xa9, 0x50, 0x60, 0x61, 0x48, 0x13, 0x77, 0x6b, 0xe9, 0xa0, 0xd2, 0x96, 0x4e, 0xe9, 0x8a, 0xc4,
+	0x55, 0xe4, 0xb5, 0x2e, 0xb2, 0x70, 0xe2, 0x28, 0x49, 0x8b, 0xfa, 0x1a, 0x3c, 0x0b, 0x2f, 0xc1,
+	0x5b, 0xa1, 0xc6, 0xf6, 0x48, 0xa3, 0xdc, 0xfd, 0x72, 0xce, 0xf1, 0x89, 0xcf, 0xb1, 0x0d, 0x08,
+	0x47, 0xf4, 0x6d, 0x80, 0x43, 0xba, 0x21, 0x49, 0x6a, 0x47, 0x31, 0x4f, 0x39, 0xd2, 0x71, 0x44,
+	0xad, 0xdf, 0x1a, 0x34, 0xef, 0x24, 0x8e, 0x5e, 0x83, 0xa1, 0x34, 0xfe, 0x8e, 0xc4, 0x09, 0xe5,
+	0xa1, 0xa9, 0x0d, 0xb4, 0x61, 0xcb, 0xeb, 0x29, 0xfc, 0xab, 0x80, 0xd1, 0x25, 0x34, 0x36, 0x0c,
+	0xef, 0x78, 0x9c, 0x98, 0x95, 0x81, 0x3e, 0x6c, 0x3b, 0x7d, 0x1b, 0x47, 0xd4, 0x56, 0x56, 0xf6,
+	0x8d, 0x20, 0xa7, 0x61, 0x1a, 0xef, 0x3d, 0x25, 0xed, 0x7f, 0x86, 0xd3, 0x3c, 0x81, 0x0c, 0xd0,
+	0x7f, 0x90, 0xbd, 0xfc, 0xc7, 0x61, 0x44, 0x2f, 0xa1, 0xb6, 0xc3, 0x6c, 0x4b, 0xcc, 0xca, 0x40,
+	0x1b, 0xb6, 0x9d, 0x76, 0xe6, 0x2a, 0xd6, 0x78, 0x82, 0xf9, 0x58, 0xb9, 0xd2, 0xac, 0x5f, 0x15,
+	0xa8, 0x0b, 0x14, 0x21, 0xa8, 0x86, 0x38, 0x20, 0xd2, 0x24, 0x9b, 0xd1, 0x2b, 0xe8, 0x6e, 0x28,
+	0x4b, 0x49, 0xec, 0x47, 0x31, 0xdf, 0x50, 0x26, 0xec, 0x5a, 0x5e, 0x47, 0xa0, 0xf7, 0x02, 0x44,
+	0x26, 0x34, 0x04, 0x90, 0x98, 0xfa, 0x40, 0x1f, 0xb6, 0x3c, 0xf5, 0x89, 0x2e, 0xa0, 0xbd, 0xe2,
+	0x41, 0xc4, 0x28, 0x0e, 0x57, 0x24, 0x31, 0xab, 0x03, 0x7d, 0xd8, 0x75, 0x7a, 0xd9, 0x66, 0x26,
+	0x4f, 0xb8, 0x97, 0xd7, 0xa0, 0x0f, 0xd0, 0x94, 0x9d, 0x25, 0x66, 0x2d, 0xab, 0xe4, 0x79, 0x6e,
+	0xf3, 0xb6, 0x2c, 0x4e, 0x36, 0xf2, 0x24, 0xed, 0xcf, 0xa0, 0x73, 0x44, 0x95, 0x74, 0x62, 0x1d,
+	0x77, 0x72, 0x9a, 0xd9, 0xca, 0x45, 0xf9, 0x52, 0xfe, 0x68, 0xd0, 0x50, 0xe7, 0x53, 0xd6, 0xca,
+	0x1c, 0xfe, 0xe3, 0x11, 0x89, 0x71, 0x4a, 0xc3, 0xef, 0x7e, 0xb2, 0x4f, 0x52, 0x12, 0xa8, 0xd3,
+	0xb3, 0xf2, 0x9e, 0xf6, 0x5c, 0xa9, 0x16, 0x42, 0x24, 0xf6, 0x6c, 0xf0, 0x02, 0xdc, 0xff, 0x06,
+	0xe7, 0xa5, 0xd2, 0x92, 0x0c, 0xa3, 0xe3, 0x0c, 0xff, 0x67, 0xff, 0x2b, 0x2c, 0xce, 0x67, 0x59,
+	0x41, 0xaf, 0xc0, 0xa2, 0x37, 0xb9, 0x48, 0x5d, 0xc7, 0x2c, 0x73, 0x70, 0x71, 0x40, 0x64, 0x58,
+	0x0b, 0xea, 0x8f, 0x5b, 0xca, 0xd6, 0x2a, 0x21, 0x64, 0xfa, 0xf1, 0x01, 0xf2, 0x24, 0x63, 0x6d,
+	0xa1, 0x96, 0x01, 0xe8, 0x0a, 0xcc, 0x62, 0x33, 0x85, 0x07, 0xf0, 0xac, 0x10, 0x5e, 0xf5, 0xec,
+	0xc0, 0xf9, 0x9a, 0xff, 0x0c, 0x19, 0xc7, 0x6b, 0x9f, 0xf1, 0x15, 0x4e, 0x29, 0x0f, 0xfd, 0x6d,
+	0xcc, 0xe4, 0x85, 0x3b, 0x53, 0xe4, 0xad, 0xe4, 0x96, 0x31, 0x1b, 0xbd, 0x00, 0xf8, 0x77, 0x89,
+	0x50, 0x07, 0x5a, 0x37, 0xb3, 0xfb, 0x85, 0x7f, 0x71, 0xf9, 0xce, 0x31, 0x4e, 0x46, 0x2e, 0x9c,
+	0x95, 0x84, 0x42, 0x00, 0xf5, 0xe5, 0x78, 0xe9, 0x3e, 0x2c, 0x8d, 0x93, 0xc3, 0xfc, 0x69, 0x3a,
+	0x9e, 0x5d, 0xbb, 0x86, 0x76, 0x98, 0x27, 0x53, 0xf7, 0x61, 0xbe, 0x30, 0x2a, 0xa8, 0x09, 0x55,
+	0xef, 0xcb, 0xf4, 0xd6, 0xd0, 0x51, 0x0b, 0x6a, 0x77, 0xd7, 0x93, 0xf9, 0xc2, 0xa8, 0x3e, 0xd6,
+	0xb3, 0xc7, 0xfe, 0xfe, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x6e, 0x32, 0x39, 0xbf, 0x02, 0x04,
+	0x00, 0x00,
 }
diff --git a/api/manifest.proto b/api/manifest.proto
index 4a8e4cd..a63934d 100644
--- a/api/manifest.proto
+++ b/api/manifest.proto
@@ -16,12 +16,12 @@ syntax = "proto3";
 package api;
 
 // Builds must be uniquely addressable from the top level so that they can be used to look up the location of binaries.
-// Format: filter_profile(-compliance_profile)/operating_system-operating_system_version:envoy_version
+// Format: filter_profile(-compliance_profile):envoy_version/operating_system-operating_system_version
 // Examples:
-//   - istio-fips1402/ubuntu-16.04:1.10.1
-//   - standard/debian-8:nightly
+//   - istio-fips1402:1.10.1/ubuntu-16.04
+//   - standard:nightly/debian-8
 message Manifest {
-   string version = 1;
+   string manifest_version = 1;
 
    // Key is composite key of the value's filter_profile and compliance_profile
    // Note: compliance_profile is optional
@@ -30,32 +30,47 @@ message Manifest {
 }
 
 message Flavor {
+   // This is duplicated in order to make flavor easier to sort
+   string name = 1;
+   
    // Filter profile is the name of the collection of filters
-   // E.g. Standard, Minimal, Istio
-   string filter_profile = 1;
+   // E.g. standard, minimal, istio
+   string filter_profile = 2;
 
    // All filters available in this flavor
-   repeated string filters = 2;
+   repeated string filters = 3;
   
-   repeated Compliance compliances = 3;
+   repeated Compliance compliances = 4;
   
-   // Key is composite key of the value's operating_system, operating_system_version and envoy_version
-   // Format: operating_system-operating_system_version:envoy_version
-   map <string, Build> builds = 5;
+   // Key is Envoy version
+   map <string, Version> versions = 5;
+}
+
+message Version {
+   // This duplicated in order to make version easier to sort
+   string name = 1;
+   // Key is operating system
+   // Note: Keys in proto maps cannot be enums, so this has to be a string.
+   map <string, OperatingSystem> operating_systems = 2; 
+}
+
+message OperatingSystem {
+   // Matches the key, duplicated here to be explicit.
+   OperatingSystemName name = 1;
+   
+   repeated Build builds = 2;
 }
 
 message Build {
-   OperatingSystem operating_system = 1;
-   string operating_system_version = 2;
-   string envoy_version = 3;
-   string download_location_url = 4;
+   string operating_system_version = 1;
+   string download_location_url = 2;
 }
 
 enum Compliance {
    FIPS_1402 = 0;
 }
 
-enum OperatingSystem {
+enum OperatingSystemName {
    UBUNTU = 0;
    DEBIAN = 1;
    CENTOS = 2;
diff --git a/ci/format b/ci/format
index 8b53979..eebda84 100755
--- a/ci/format
+++ b/ci/format
@@ -4,4 +4,4 @@
 find api -type f -name '*.proto' | xargs protoc --go_out=.
 
 # Apply Licenses
-licenser apply -r . "Tetrate"
+licenser apply -r "Tetrate"
diff --git a/ci/install b/ci/install
new file mode 100755
index 0000000..3616204
--- /dev/null
+++ b/ci/install
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# Install Licenser
+wget -c https://github.com/liamawhite/licenser/releases/download/v0.5.1/licenser_0.5.1_Linux_x86_64.tar.gz -O - | sudo tar -xz -C /usr/bin
+
+# Install Proto compiler
+wget https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-linux-x86_64.zip
+unzip protoc-3.8.0-linux-x86_64.zip -d protoc
+sudo mv protoc/bin/* /usr/bin/
+sudo mv protoc/include/* /usr/include/
+rm -rf protoc*
+
+# Install Golang Proto generator
+go get -u github.com/golang/protobuf/protoc-gen-go
diff --git a/ci/lint b/ci/lint
index b24b32d..161d2f1 100755
--- a/ci/lint
+++ b/ci/lint
@@ -15,7 +15,7 @@ fi
 # NOTE: This must be done after license verification as we need to
 #       generate licenses as part of proto generation.
 find api -type f -name '*.proto' | xargs protoc --go_out=.
-licenser apply -r . "Tetrate"
+licenser apply -r "Tetrate"
 if [ "$(git status --porcelain)" != "" ]; then
     echo "The following Protos need to be regenerated:"
     git status --porcelain -v
diff --git a/cmd/getenvoy/main.go b/cmd/getenvoy/main.go
new file mode 100644
index 0000000..276b4c9
--- /dev/null
+++ b/cmd/getenvoy/main.go
@@ -0,0 +1,29 @@
+// Copyright 2019 Tetrate
+//
+// 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 main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/tetratelabs/getenvoy/pkg/cmd"
+)
+
+func main() {
+	if err := cmd.RootCmd.Execute(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a59ac34
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,9 @@
+module github.com/tetratelabs/getenvoy
+
+go 1.12
+
+require (
+	github.com/golang/protobuf v1.3.1
+	github.com/spf13/cobra v0.0.5
+	github.com/stretchr/testify v1.2.2
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f070261
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,49 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go
new file mode 100644
index 0000000..a6f1512
--- /dev/null
+++ b/pkg/cmd/root.go
@@ -0,0 +1,29 @@
+// Copyright 2019 Tetrate
+//
+// 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 cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+var cliVersion string
+
+// RootCmd represents the base command when called without any subcommands
+var RootCmd = &cobra.Command{
+	Use:   "getenvoy",
+	Short: "getenvoy",
+	Long:  "getenvoy",
+	Version: cliVersion,
+}
diff --git a/pkg/manifest/fetch.go b/pkg/manifest/fetch.go
new file mode 100644
index 0000000..46226cd
--- /dev/null
+++ b/pkg/manifest/fetch.go
@@ -0,0 +1,40 @@
+// Copyright 2019 Tetrate
+//
+// 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 manifest
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/tetratelabs/getenvoy/api"
+)
+
+// Fetch retrieves and parses a manifest from the URL passed
+func Fetch(manifestURL string) (*api.Manifest, error) {
+	resp, err := http.Get(manifestURL)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("received %q from %v", resp.StatusCode, manifestURL)
+	}
+	defer resp.Body.Close()
+	result := api.Manifest{}
+	if err := jsonpb.Unmarshal(resp.Body, &result); err != nil {
+		return nil, fmt.Errorf("error unmarshalling manifest: %v", err)
+	}
+	return &result, nil
+}
diff --git a/pkg/manifest/print.go b/pkg/manifest/print.go
new file mode 100644
index 0000000..3ab1466
--- /dev/null
+++ b/pkg/manifest/print.go
@@ -0,0 +1,70 @@
+// Copyright 2019 Tetrate
+//
+// 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 manifest
+
+import (
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/tetratelabs/getenvoy/api"
+)
+
+// PrettyPrint tabwrites the passed manifest to the passed writer
+func PrettyPrint(writer io.Writer, manifest *api.Manifest) error {
+	w := new(tabwriter.Writer).Init(writer, 0, 8, 5, ' ', 0)
+	fmt.Fprintln(w, "FLAVOR\tVERSION\tAVAILABLE ON")
+
+	for _, flavor := range deterministicFlavors(manifest.Flavors) {
+		for _, version := range deterministicVersions(flavor.Versions) {
+			osList := []string{}
+			for os := range version.GetOperatingSystems() {
+				osList = append(osList, os)
+			}
+			sort.Slice(osList, func(i, j int) bool {
+				return strings.ToUpper(osList[i]) < strings.ToUpper(osList[j])
+			})
+			prettyOS := strings.Trim(fmt.Sprintf("%v", osList), "[]")
+			fmt.Fprintf(w, "%v\t%v\t%v\n", flavor.Name, version.Name, prettyOS)
+		}
+	}
+	return w.Flush()
+}
+
+func deterministicFlavors(flavors map[string]*api.Flavor) []*api.Flavor {
+	flavorList := []*api.Flavor{}
+	for _, flavor := range flavors {
+		flavorList = append(flavorList, flavor)
+	}
+	sort.Slice(flavorList, func(i, j int) bool {
+		return flavorList[i].Name < flavorList[j].Name
+	})
+	return flavorList
+}
+
+func deterministicVersions(versions map[string]*api.Version) []*api.Version {
+	versionList := []*api.Version{}
+	for _, version := range versions {
+		versionList = append(versionList, version)
+	}
+
+	sort.Slice(versionList, func(i, j int) bool {
+		// Note: version is reverse alphabetical so that newer versions are first
+		return versionList[i].Name > versionList[j].Name
+	})
+	return versionList
+}


Test patch:
diff --git a/ci/test b/ci/test
new file mode 100755
index 0000000..fea507d
--- /dev/null
+++ b/ci/test
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+go test -timeout 30s ./...
diff --git a/pkg/manifest/fetch_test.go b/pkg/manifest/fetch_test.go
new file mode 100644
index 0000000..c900162
--- /dev/null
+++ b/pkg/manifest/fetch_test.go
@@ -0,0 +1,186 @@
+// Copyright 2019 Tetrate
+//
+// 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 manifest
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/tetratelabs/getenvoy/api"
+)
+
+func TestFetch(t *testing.T) {
+	tests := []struct {
+		name                 string
+		responseStatusCode   int
+		responseManifestFile string
+		want                 *api.Manifest
+		wantErr              bool
+	}{
+		{
+			name:                 "responds with parsed manifest",
+			responseStatusCode:   http.StatusOK,
+			responseManifestFile: "manifest.golden",
+			want:                 goodManifest(),
+		},
+		{
+			name:               "errors on non-200 response",
+			responseStatusCode: http.StatusInternalServerError,
+			want:               nil,
+			wantErr:            true,
+		},
+		{
+			name:                 "errors on unparsable manifest",
+			responseStatusCode:   http.StatusOK,
+			responseManifestFile: "malformed.golden",
+			wantErr:              true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mock := mockServer(tt.responseStatusCode, tt.responseManifestFile)
+			defer mock.Close()
+			got, err := Fetch(mock.URL)
+			assert.Equal(t, tt.want, got)
+			if tt.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func mockServer(responseStatusCode int, responseManifestFile string) *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(responseStatusCode)
+		if responseStatusCode == http.StatusOK {
+			bytes, _ := ioutil.ReadFile(filepath.Join("testdata", responseManifestFile))
+			w.Write(bytes)
+		}
+	}))
+}
+
+func goodManifest() *api.Manifest {
+	return &api.Manifest{
+		ManifestVersion: "v0.1.0",
+		Flavors: map[string]*api.Flavor{
+			"standard": &api.Flavor{
+				Name:          "standard",
+				FilterProfile: "standard",
+				Versions: map[string]*api.Version{
+					"1.10.0": &api.Version{
+						Name: "1.10.0",
+						OperatingSystems: map[string]*api.OperatingSystem{
+							"Ubuntu": &api.OperatingSystem{
+								Name: api.OperatingSystemName_UBUNTU,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "16.04",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+							"macOS": &api.OperatingSystem{
+								Name: api.OperatingSystemName_MACOS,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "10.14",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+							"CentOS": &api.OperatingSystem{
+								Name: api.OperatingSystemName_CENTOS,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "7",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+						},
+					},
+					"nightly": &api.Version{
+						Name: "nightly",
+						OperatingSystems: map[string]*api.OperatingSystem{
+							"Ubuntu": &api.OperatingSystem{
+								Name: api.OperatingSystemName_UBUNTU,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "16.04",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+							"macOS": &api.OperatingSystem{
+								Name: api.OperatingSystemName_MACOS,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "10.14",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+							"CentOS": &api.OperatingSystem{
+								Name: api.OperatingSystemName_CENTOS,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "7",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			"standard-fips1402": &api.Flavor{
+				Name:          "standard-fips1402",
+				FilterProfile: "standard",
+				Compliances:   []api.Compliance{api.Compliance_FIPS_1402},
+				Versions: map[string]*api.Version{
+					"1.10.0": &api.Version{
+						Name: "1.10.0",
+						OperatingSystems: map[string]*api.OperatingSystem{
+							"Ubuntu": &api.OperatingSystem{
+								Name: api.OperatingSystemName_UBUNTU,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "16.04",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+							"CentOS": &api.OperatingSystem{
+								Name: api.OperatingSystemName_CENTOS,
+								Builds: []*api.Build{
+									&api.Build{
+										OperatingSystemVersion: "7",
+										DownloadLocationUrl:    "http://example.com",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
diff --git a/pkg/manifest/print_test.go b/pkg/manifest/print_test.go
new file mode 100644
index 0000000..16d7b57
--- /dev/null
+++ b/pkg/manifest/print_test.go
@@ -0,0 +1,44 @@
+// Copyright 2019 Tetrate
+//
+// 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 manifest
+
+import (
+	"bytes"
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPrettyPrint(t *testing.T) {
+	tests := []struct {
+		name           string
+		wantOutputFile string
+	}{
+		{
+			name:           "Prints golden output",
+			wantOutputFile: "list.golden",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := bytes.NewBuffer(nil)
+			PrettyPrint(got, goodManifest())
+			want, _ := ioutil.ReadFile(filepath.Join("testdata", tt.wantOutputFile))
+			assert.Equal(t, string(want), got.String())
+		})
+	}
+}
diff --git a/pkg/manifest/testdata/list.golden b/pkg/manifest/testdata/list.golden
new file mode 100644
index 0000000..1272545
--- /dev/null
+++ b/pkg/manifest/testdata/list.golden
@@ -0,0 +1,4 @@
+FLAVOR                VERSION     AVAILABLE ON
+standard              nightly     CentOS macOS Ubuntu
+standard              1.10.0      CentOS macOS Ubuntu
+standard-fips1402     1.10.0      CentOS Ubuntu
diff --git a/pkg/manifest/testdata/malformed.golden b/pkg/manifest/testdata/malformed.golden
new file mode 100644
index 0000000..1252274
--- /dev/null
+++ b/pkg/manifest/testdata/malformed.golden
@@ -0,0 +1,1 @@
+NotAJSONFile
diff --git a/pkg/manifest/testdata/manifest.golden b/pkg/manifest/testdata/manifest.golden
new file mode 100644
index 0000000..5d79b7f
--- /dev/null
+++ b/pkg/manifest/testdata/manifest.golden
@@ -0,0 +1,102 @@
+{
+  "manifestVersion": "v0.1.0",
+  "flavors": {
+    "standard": {
+      "name": "standard",
+      "filterProfile": "standard",
+      "versions": {
+        "1.10.0": {
+          "name": "1.10.0",
+          "operatingSystems": {
+            "CentOS": {
+              "name": "CENTOS",
+              "builds": [
+                {
+                  "operatingSystemVersion": "7",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            },
+            "macOS": {
+              "name": "MACOS",
+              "builds": [
+                {
+                  "operatingSystemVersion": "10.14",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            },
+            "Ubuntu": {
+              "builds": [
+                {
+                  "operatingSystemVersion": "16.04",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            }
+          }
+        },
+        "nightly": {
+          "name": "nightly",
+          "operatingSystems": {
+            "CentOS": {
+              "name": "CENTOS",
+              "builds": [
+                {
+                  "operatingSystemVersion": "7",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            },
+            "macOS": {
+              "name": "MACOS",
+              "builds": [
+                {
+                  "operatingSystemVersion": "10.14",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            },
+            "Ubuntu": {
+              "builds": [
+                {
+                  "operatingSystemVersion": "16.04",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            }
+          }
+        }
+      }
+    },
+    "standard-fips1402": {
+      "name": "standard-fips1402",
+      "filterProfile": "standard",
+      "compliances": ["FIPS_1402"],
+      "versions": {
+        "1.10.0": {
+          "name": "1.10.0",
+          "operatingSystems": {
+            "CentOS": {
+              "name": "CENTOS",
+              "builds": [
+                {
+                  "operatingSystemVersion": "7",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            },
+            "Ubuntu": {
+              "builds": [
+                {
+                  "operatingSystemVersion": "16.04",
+                  "downloadLocationUrl": "http://example.com"
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}