From 2dbd7ecc14ab382338488d3dc8eb4b2069351a7e Mon Sep 17 00:00:00 2001
From: Jessica Yuen <im.jessicayuen@gmail.com>
Date: Wed, 31 Jan 2018 10:46:26 -0800
Subject: [PATCH] Add EnvironmentSpec to app.yaml

Signed-off-by: Jessica Yuen <im.jessicayuen@gmail.com>
---
 metadata/app/schema.go      | 125 ++++++++++++++++++++++-------
 metadata/app/schema_test.go | 152 +++++++++++++++++++++++++++++++++++-
 metadata/manager.go         |  11 +--
 3 files changed, 253 insertions(+), 35 deletions(-)

diff --git a/metadata/app/schema.go b/metadata/app/schema.go
index c3c720e9..1db429f8 100644
--- a/metadata/app/schema.go
+++ b/metadata/app/schema.go
@@ -29,8 +29,14 @@ const (
 	DefaultVersion    = "0.0.1"
 )
 
-var ErrRegistryNameInvalid = fmt.Errorf("Registry name is invalid")
-var ErrRegistryExists = fmt.Errorf("Registry with name already exists")
+var (
+	ErrRegistryNameInvalid = fmt.Errorf("Registry name is invalid")
+	ErrRegistryExists      = fmt.Errorf("Registry with name already exists")
+	// ErrEnvironmentNameInvalid is the error where an environment name is invalid.
+	ErrEnvironmentNameInvalid = fmt.Errorf("Environment name is invalid")
+	// ErrEnvironmentExists is the error when trying to create an environment that already exists.
+	ErrEnvironmentExists = fmt.Errorf("Environment with name already exists")
+)
 
 type Spec struct {
 	APIVersion   string           `json:"apiVersion,omitempty"`
@@ -44,10 +50,76 @@ type Spec struct {
 	Bugs         string           `json:"bugs,omitempty"`
 	Keywords     []string         `json:"keywords,omitempty"`
 	Registries   RegistryRefSpecs `json:"registries,omitempty"`
+	Environments EnvironmentSpecs `json:"environments,omitempty"`
 	Libraries    LibraryRefSpecs  `json:"libraries,omitempty"`
 	License      string           `json:"license,omitempty"`
 }
 
+type RepositorySpec struct {
+	Type string `json:"type"`
+	URI  string `json:"uri"`
+}
+
+type RegistryRefSpec struct {
+	Name       string          `json:"-"`
+	Protocol   string          `json:"protocol"`
+	URI        string          `json:"uri"`
+	GitVersion *GitVersionSpec `json:"gitVersion"`
+}
+
+type RegistryRefSpecs map[string]*RegistryRefSpec
+
+// EnvironmentSpecs contains one or more EnvironmentSpec.
+type EnvironmentSpecs map[string]*EnvironmentSpec
+
+// EnvironmentSpec contains the specification for ksonnet environments.
+//
+// KubernetesVersion: The Kubernetes version the target cluster is running on.
+// Destinations:      One or more cluster addresses that this environment
+//                    points to.
+// Targets:           The relative component paths that this environment wishes
+//                    to deploy onto it's destinations.
+type EnvironmentSpec struct {
+	Name              string                      `json:"-"`
+	KubernetesVersion string                      `json:"k8sVersion"`
+	Destinations      EnvironmentDestinationSpecs `json:"destinations"`
+	Targets           []string                    `json:"targets"`
+}
+
+// EnvironmentDestinationSpecs contains one or more EnvironmentDestinationSpec.
+type EnvironmentDestinationSpecs []*EnvironmentDestinationSpec
+
+// EnvironmentDestinationSpec contains the specification for the cluster
+// addresses that the environment points to.
+//
+// Server:    The Kubernetes server that the cluster is running on.
+// Namespace: The namespace of the Kubernetes server that targets should
+//            be deployed to. This is "default", by default.
+type EnvironmentDestinationSpec struct {
+	Server    string `json:"server"`
+	Namespace string `json:"namespace"`
+}
+
+type LibraryRefSpec struct {
+	Name       string          `json:"name"`
+	Registry   string          `json:"registry"`
+	GitVersion *GitVersionSpec `json:"gitVersion"`
+}
+
+type GitVersionSpec struct {
+	RefSpec   string `json:"refSpec"`
+	CommitSHA string `json:"commitSha"`
+}
+
+type LibraryRefSpecs map[string]*LibraryRefSpec
+
+type ContributorSpec struct {
+	Name  string `json:"name"`
+	Email string `json:"email"`
+}
+
+type ContributorSpecs []*ContributorSpec
+
 func Unmarshal(bytes []byte) (*Spec, error) {
 	schema := Spec{}
 	err := yaml.Unmarshal(bytes, &schema)
@@ -105,36 +177,33 @@ func (s *Spec) validate() error {
 	return nil
 }
 
-type RepositorySpec struct {
-	Type string `json:"type"`
-	URI  string `json:"uri"`
-}
-
-type RegistryRefSpec struct {
-	Name       string          `json:"-"`
-	Protocol   string          `json:"protocol"`
-	URI        string          `json:"uri"`
-	GitVersion *GitVersionSpec `json:"gitVersion"`
+// GetEnvironmentSpec returns the environment specification for the environment.
+func (s *Spec) GetEnvironmentSpec(name string) (*EnvironmentSpec, bool) {
+	environmentSpec, ok := s.Environments[name]
+	if ok {
+		environmentSpec.Name = name
+	}
+	return environmentSpec, ok
 }
 
-type RegistryRefSpecs map[string]*RegistryRefSpec
+// AddEnvironmentSpec adds an EnvironmentSpec to the list of EnvironmentSpecs.
+// This is equivalent to registering the environment for a ksonnet app.
+func (s *Spec) AddEnvironmentSpec(spec *EnvironmentSpec) error {
+	if spec.Name == "" {
+		return ErrEnvironmentNameInvalid
+	}
 
-type LibraryRefSpec struct {
-	Name       string          `json:"name"`
-	Registry   string          `json:"registry"`
-	GitVersion *GitVersionSpec `json:"gitVersion"`
-}
+	_, environmentSpecExists := s.Environments[spec.Name]
+	if environmentSpecExists {
+		return ErrEnvironmentExists
+	}
 
-type GitVersionSpec struct {
-	RefSpec   string `json:"refSpec"`
-	CommitSHA string `json:"commitSha"`
+	s.Environments[spec.Name] = spec
+	return nil
 }
 
-type LibraryRefSpecs map[string]*LibraryRefSpec
-
-type ContributorSpec struct {
-	Name  string `json:"name"`
-	Email string `json:"email"`
+// DeleteEnvironmentSpec removes the environment specification from the app spec.
+func (s *Spec) DeleteEnvironmentSpec(name string) error {
+	delete(s.Environments, name)
+	return nil
 }
-
-type ContributorSpecs []*ContributorSpec
diff --git a/metadata/app/schema_test.go b/metadata/app/schema_test.go
index c56112d4..b0036777 100644
--- a/metadata/app/schema_test.go
+++ b/metadata/app/schema_test.go
@@ -16,7 +16,6 @@
 package app
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/blang/semver"
@@ -34,6 +33,19 @@ func makeSimpleRefSpec(name, protocol, uri, version string) *RegistryRefSpec {
 	}
 }
 
+func makeSimpleEnvironmentSpec(name, namespace, server, k8sVersion string) *EnvironmentSpec {
+	return &EnvironmentSpec{
+		Name: name,
+		Destinations: EnvironmentDestinationSpecs{
+			&EnvironmentDestinationSpec{
+				Namespace: namespace,
+				Server:    server,
+			},
+		},
+		KubernetesVersion: k8sVersion,
+	}
+}
+
 func TestApiVersionValidate(t *testing.T) {
 	type spec struct {
 		spec string
@@ -87,7 +99,6 @@ func TestGetRegistryRefSuccess(t *testing.T) {
 	}
 
 	r1, ok := example1.GetRegistryRef("simple1")
-	fmt.Println(r1)
 	if r1 == nil || !ok {
 		t.Error("Expected registry to contain 'simple1'")
 	}
@@ -159,3 +170,140 @@ func TestAddRegistryRefFailure(t *testing.T) {
 		t.Error("Expected registry to fail to add registry with duplicate name and different uri")
 	}
 }
+
+func TestGetEnvironmentSpecSuccess(t *testing.T) {
+	const (
+		env        = "dev"
+		namespace  = "default"
+		server     = "http://example.com"
+		k8sVersion = "1.8.0"
+	)
+
+	example1 := Spec{
+		Environments: EnvironmentSpecs{
+			env: &EnvironmentSpec{
+				Destinations: EnvironmentDestinationSpecs{
+					&EnvironmentDestinationSpec{
+						Namespace: namespace,
+						Server:    server,
+					},
+				},
+				KubernetesVersion: k8sVersion,
+			},
+		},
+	}
+
+	r1, ok := example1.GetEnvironmentSpec(env)
+	if r1 == nil || !ok {
+		t.Errorf("Expected environments to contain '%s'", env)
+	}
+
+	if len(r1.Destinations) != 1 || r1.Destinations[0].Namespace != namespace ||
+		r1.Destinations[0].Server != server || r1.KubernetesVersion != k8sVersion {
+		t.Errorf("Environment did not add correct values:\n%s", r1)
+	}
+}
+
+func TestGetEnvironmentSpecFailure(t *testing.T) {
+	example1 := Spec{
+		Environments: EnvironmentSpecs{
+			"dev": &EnvironmentSpec{
+				Destinations: EnvironmentDestinationSpecs{
+					&EnvironmentDestinationSpec{
+						Namespace: "default",
+						Server:    "http://example.com",
+					},
+				},
+				KubernetesVersion: "1.8.0",
+			},
+		},
+	}
+
+	r1, ok := example1.GetEnvironmentSpec("prod")
+	if r1 != nil || ok {
+		t.Error("Expected environemnts to not contain 'prod'")
+	}
+}
+
+func TestAddEnvironmentSpecSuccess(t *testing.T) {
+	const (
+		env        = "dev"
+		namespace  = "default"
+		server     = "http://example.com"
+		k8sVersion = "1.8.0"
+	)
+
+	example1 := Spec{
+		Environments: EnvironmentSpecs{},
+	}
+
+	err := example1.AddEnvironmentSpec(makeSimpleEnvironmentSpec(env, namespace, server, k8sVersion))
+	if err != nil {
+		t.Errorf("Expected environment add to succeed:\n%s", err)
+	}
+
+	r1, ok1 := example1.GetEnvironmentSpec(env)
+	if !ok1 || len(r1.Destinations) != 1 || r1.Destinations[0].Namespace != namespace ||
+		r1.Destinations[0].Server != server || r1.KubernetesVersion != k8sVersion {
+		t.Errorf("Environment did not add correct values:\n%s", r1)
+	}
+}
+
+func TestAddEnvironmentSpecFailure(t *testing.T) {
+	const (
+		envName1   = "dev"
+		envName2   = ""
+		namespace  = "default"
+		server     = "http://example.com"
+		k8sVersion = "1.8.0"
+	)
+
+	example1 := Spec{
+		Environments: EnvironmentSpecs{
+			envName1: &EnvironmentSpec{
+				Destinations: EnvironmentDestinationSpecs{
+					&EnvironmentDestinationSpec{
+						Namespace: namespace,
+						Server:    server,
+					},
+				},
+				KubernetesVersion: k8sVersion,
+			},
+		},
+	}
+
+	err := example1.AddEnvironmentSpec(makeSimpleEnvironmentSpec(envName2, namespace, server, k8sVersion))
+	if err != ErrEnvironmentNameInvalid {
+		t.Error("Expected failure while adding environment with an invalid name")
+	}
+
+	err = example1.AddEnvironmentSpec(makeSimpleEnvironmentSpec(envName1, namespace, server, k8sVersion))
+	if err != ErrEnvironmentExists {
+		t.Error("Expected failure while adding environment with duplicate name")
+	}
+}
+
+func TestDeleteEnvironmentSpec(t *testing.T) {
+	example1 := Spec{
+		Environments: EnvironmentSpecs{
+			"dev": &EnvironmentSpec{
+				Destinations: EnvironmentDestinationSpecs{
+					&EnvironmentDestinationSpec{
+						Namespace: "default",
+						Server:    "http://example.com",
+					},
+				},
+				KubernetesVersion: "1.8.0",
+			},
+		},
+	}
+
+	err := example1.DeleteEnvironmentSpec("dev")
+	if err != nil {
+		t.Error("Expected to successfully delete an environment in spec")
+	}
+
+	if _, ok := example1.GetEnvironmentSpec("dev"); ok {
+		t.Error("Expected environment 'dev' to be deleted from spec, but still exists")
+	}
+}
diff --git a/metadata/manager.go b/metadata/manager.go
index 19128b3c..ac400a85 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -395,11 +395,12 @@ func generateRegistryYAMLData(incubatorReg registry.Manager) ([]byte, error) {
 
 func generateAppYAMLData(name string, refs ...*app.RegistryRefSpec) ([]byte, error) {
 	content := app.Spec{
-		APIVersion: app.DefaultAPIVersion,
-		Kind:       app.Kind,
-		Name:       name,
-		Version:    app.DefaultVersion,
-		Registries: app.RegistryRefSpecs{},
+		APIVersion:   app.DefaultAPIVersion,
+		Kind:         app.Kind,
+		Name:         name,
+		Version:      app.DefaultVersion,
+		Registries:   app.RegistryRefSpecs{},
+		Environments: app.EnvironmentSpecs{},
 	}
 
 	for _, ref := range refs {
-- 
GitLab