From fdc16739f6669dcb8043e5aa12f988e417d1044d Mon Sep 17 00:00:00 2001 From: jackgr Date: Thu, 26 Nov 2015 11:11:47 -0800 Subject: [PATCH] First set of changes for persistent repository. --- common/types.go | 1 - install-local.yaml | 167 ++++++ manager/deployments.go | 51 +- manager/manager/deployer.go | 4 +- manager/manager/manager.go | 40 +- manager/manager/manager_test.go | 57 +- manager/repository/persistent/persistent.go | 488 ++++++++++++++++++ .../repository/persistent/persistent_test.go | 110 ++++ manager/repository/repository.go | 327 +----------- .../{repository_test.go => test_common.go} | 149 ++++-- manager/repository/transient/transient.go | 325 ++++++++++++ .../repository/transient/transient_test.go | 55 ++ 12 files changed, 1355 insertions(+), 419 deletions(-) create mode 100644 install-local.yaml create mode 100644 manager/repository/persistent/persistent.go create mode 100644 manager/repository/persistent/persistent_test.go rename manager/repository/{repository_test.go => test_common.go} (67%) create mode 100644 manager/repository/transient/transient.go create mode 100644 manager/repository/transient/transient_test.go diff --git a/common/types.go b/common/types.go index e09327c13..67ef834a6 100644 --- a/common/types.go +++ b/common/types.go @@ -35,7 +35,6 @@ type Schema struct { // the creation, modification and/or deletion of a set of resources. type Deployment struct { Name string `json:"name"` - ID int `json:"id"` CreatedAt time.Time `json:"createdAt,omitempty"` DeployedAt time.Time `json:"deployedAt,omitempty"` ModifiedAt time.Time `json:"modifiedAt,omitempty"` diff --git a/install-local.yaml b/install-local.yaml new file mode 100644 index 000000000..6758e5a05 --- /dev/null +++ b/install-local.yaml @@ -0,0 +1,167 @@ +###################################################################### +# Copyright 2015 The Kubernetes Authors All rights reserved. +# 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. +###################################################################### + +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + app: dm + name: dm-namespace + name: dm +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: dm + name: expandybird-service + name: expandybird-service + namespace: dm +spec: + ports: + - name: expandybird + port: 8081 + targetPort: 8080 + selector: + app: dm + name: expandybird +--- +apiVersion: v1 +kind: ReplicationController +metadata: + labels: + app: dm + name: expandybird-rc + name: expandybird-rc + namespace: dm +spec: + replicas: 2 + selector: + app: dm + name: expandybird + template: + metadata: + labels: + app: dm + name: expandybird + spec: + containers: + - env: [] + image: gcr.io/dm-k8s-test-jackgr/expandybird:latest + name: expandybird + ports: + - containerPort: 8080 + name: expandybird +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: dm + name: resourcifier-service + name: resourcifier-service + namespace: dm +spec: + ports: + - name: resourcifier + port: 8082 + targetPort: 8080 + selector: + app: dm + name: resourcifier +--- +apiVersion: v1 +kind: ReplicationController +metadata: + labels: + app: dm + name: resourcifier-rc + name: resourcifier-rc + namespace: dm +spec: + replicas: 2 + selector: + app: dm + name: resourcifier + template: + metadata: + labels: + app: dm + name: resourcifier + spec: + containers: + - env: [] + image: gcr.io/dm-k8s-test-jackgr/resourcifier:latest + imagePullPolicy: Always + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 30 + timeoutSeconds: 1 + name: resourcifier + ports: + - containerPort: 8080 + name: resourcifier +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: dm + name: manager-service + name: manager-service + namespace: dm +spec: + ports: + - name: manager + port: 8080 + targetPort: 8080 + selector: + app: dm + name: manager +--- +apiVersion: v1 +kind: ReplicationController +metadata: + labels: + app: dm + name: manager-rc + name: manager-rc + namespace: dm +spec: + replicas: 1 + selector: + app: dm + name: manager + template: + metadata: + labels: + app: dm + name: manager + spec: + containers: + - env: [] + image: gcr.io/dm-k8s-test-jackgr/manager:latest + imagePullPolicy: Always + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 30 + timeoutSeconds: 1 + name: manager + ports: + - containerPort: 8080 + name: manager diff --git a/manager/deployments.go b/manager/deployments.go index 74ec7038d..8134c8e63 100644 --- a/manager/deployments.go +++ b/manager/deployments.go @@ -36,6 +36,8 @@ import ( "github.com/kubernetes/deployment-manager/common" "github.com/kubernetes/deployment-manager/manager/manager" "github.com/kubernetes/deployment-manager/manager/repository" + "github.com/kubernetes/deployment-manager/manager/repository/persistent" + "github.com/kubernetes/deployment-manager/manager/repository/transient" "github.com/kubernetes/deployment-manager/registry" "github.com/kubernetes/deployment-manager/util" ) @@ -69,6 +71,9 @@ var ( deployerURL = flag.String("deployerURL", "", "The URL for the deployer service.") credentialFile = flag.String("credentialFile", "", "Local file to use for credentials.") credentialSecrets = flag.Bool("credentialSecrets", true, "Use secrets for credentials.") + mongoName = flag.String("mongoName", "mongodb", "The DNS name of the mongodb service.") + mongoPort = flag.String("mongoPort", "27017", "The port of the mongodb service.") + mongoAddress = flag.String("mongoAddress", "mongodb:27017", "The address of the mongodb service.") ) var backend manager.Manager @@ -97,27 +102,39 @@ func init() { backend = newManager(credentialProvider) } +const expanderPort = "8080" +const deployerPort = "8080" + func newManager(cp common.CredentialProvider) manager.Manager { service := registry.NewInmemRegistryService() registryProvider := registry.NewDefaultRegistryProvider(cp, service) resolver := manager.NewTypeResolver(registryProvider, util.DefaultHTTPClient()) - expander := manager.NewExpander(getServiceURL(*expanderURL, *expanderName), resolver) - deployer := manager.NewDeployer(getServiceURL(*deployerURL, *deployerName)) - r := repository.NewMapBasedRepository() - credentialProvider := cp - return manager.NewManager(expander, deployer, r, registryProvider, service, credentialProvider) + expander := manager.NewExpander(getServiceURL(*expanderURL, *expanderName, expanderPort), resolver) + deployer := manager.NewDeployer(getServiceURL(*deployerURL, *deployerName, deployerPort)) + address := strings.TrimPrefix(getServiceURL(*mongoAddress, *mongoName, *mongoPort), "https://") + repository := createRepository(address) + return manager.NewManager(expander, deployer, repository, registryProvider, service, cp) +} + +func createRepository(address string) repository.Repository { + r, err := persistent.NewRepository(address) + if err != nil { + r = transient.NewRepository() + } + + return r } -func getServiceURL(serviceURL, serviceName string) string { +func getServiceURL(serviceURL, serviceName, servicePort string) string { if serviceURL == "" { serviceURL = makeEnvVariableURL(serviceName) if serviceURL == "" { addrs, err := net.LookupHost(serviceName) if err != nil || len(addrs) < 1 { - log.Fatalf("cannot resolve service:%v. environment:%v", serviceName, os.Environ()) + log.Fatalf("cannot resolve service:%v. environment:%v\n", serviceName, os.Environ()) } - serviceURL = fmt.Sprintf("https://%s", addrs[0]) + serviceURL = fmt.Sprintf("https://%s:%s", addrs[0], servicePort) } } @@ -130,7 +147,7 @@ func getServiceURL(serviceURL, serviceName string) string { func makeEnvVariableURL(str string) string { prefix := makeEnvVariableName(str) url := os.Getenv(prefix + "_PORT") - return strings.Replace(url, "tcp", "http", 1) + return strings.Replace(url, "tcp", "https", 1) } // makeEnvVariableName is copied from the Kubernetes source, @@ -335,7 +352,13 @@ func expandHandlerFunc(w http.ResponseWriter, r *http.Request) { func listTypesHandlerFunc(w http.ResponseWriter, r *http.Request) { handler := "manager: list types" util.LogHandlerEntry(handler, r) - util.LogHandlerExitWithJSON(handler, w, backend.ListTypes(), http.StatusOK) + types, err := backend.ListTypes() + if err != nil { + util.LogAndReturnError(handler, http.StatusBadRequest, err, w) + return + } + + util.LogHandlerExitWithJSON(handler, w, types, http.StatusOK) } func listTypeInstancesHandlerFunc(w http.ResponseWriter, r *http.Request) { @@ -346,7 +369,13 @@ func listTypeInstancesHandlerFunc(w http.ResponseWriter, r *http.Request) { return } - util.LogHandlerExitWithJSON(handler, w, backend.ListInstances(typeName), http.StatusOK) + instances, err := backend.ListInstances(typeName) + if err != nil { + util.LogAndReturnError(handler, http.StatusBadRequest, err, w) + return + } + + util.LogHandlerExitWithJSON(handler, w, instances, http.StatusOK) } // Putting Registry handlers here for now because deployments.go diff --git a/manager/manager/deployer.go b/manager/manager/deployer.go index 14fe71494..953f5a806 100644 --- a/manager/manager/deployer.go +++ b/manager/manager/deployer.go @@ -21,11 +21,11 @@ import ( "fmt" "io" "io/ioutil" - "time" "log" "net/http" "net/url" "strings" + "time" "github.com/ghodss/yaml" "github.com/kubernetes/deployment-manager/common" @@ -47,7 +47,7 @@ func NewDeployer(url string) Deployer { type deployer struct { deployerURL string - timeout int + timeout int } func (d *deployer) getBaseURL() string { diff --git a/manager/manager/manager.go b/manager/manager/manager.go index da1b448c0..1245445aa 100644 --- a/manager/manager/manager.go +++ b/manager/manager/manager.go @@ -44,8 +44,8 @@ type Manager interface { Expand(t *common.Template) (*common.Manifest, error) // Types - ListTypes() []string - ListInstances(typeName string) []*common.TypeInstance + ListTypes() ([]string, error) + ListInstances(typeName string) ([]*common.TypeInstance, error) // Registries ListRegistries() ([]*common.Registry, error) @@ -141,7 +141,7 @@ func (m *manager) CreateDeployment(t *common.Template) (*common.Deployment, erro return nil, err } - if err := m.repository.AddManifest(t.Name, manifest); err != nil { + if err := m.repository.AddManifest(manifest); err != nil { log.Printf("AddManifest failed %v", err) m.repository.SetDeploymentState(t.Name, failState(err)) return nil, err @@ -173,14 +173,14 @@ func (m *manager) CreateDeployment(t *common.Template) (*common.Deployment, erro // Update the manifest with the actual state of the reified resources manifest.ExpandedConfig = actualConfig - if err := m.repository.SetManifest(t.Name, manifest); err != nil { + if err := m.repository.SetManifest(manifest); err != nil { log.Printf("SetManifest failed %v", err) m.repository.SetDeploymentState(t.Name, failState(err)) return nil, err } // Finally update the type instances for this deployment. - m.addTypeInstances(t.Name, manifest.Name, manifest.Layout) + m.setTypeInstances(t.Name, manifest.Name, manifest.Layout) return m.repository.GetValidDeployment(t.Name) } @@ -200,15 +200,15 @@ func (m *manager) createManifest(t *common.Template) (*common.Manifest, error) { }, nil } -func (m *manager) addTypeInstances(deploymentName string, manifestName string, layout *common.Layout) { - m.repository.ClearTypeInstances(deploymentName) +func (m *manager) setTypeInstances(deploymentName string, manifestName string, layout *common.Layout) { + m.repository.ClearTypeInstancesForDeployment(deploymentName) instances := make(map[string][]*common.TypeInstance) for i, r := range layout.Resources { addTypeInstances(&instances, r, deploymentName, manifestName, fmt.Sprintf("$.resources[%d]", i)) } - m.repository.SetTypeInstances(deploymentName, instances) + m.repository.AddTypeInstances(instances) } func addTypeInstances(instances *map[string][]*common.TypeInstance, r *common.LayoutResource, deploymentName string, manifestName string, jsonPath string) { @@ -220,6 +220,7 @@ func addTypeInstances(instances *map[string][]*common.TypeInstance, r *common.La Manifest: manifestName, Path: jsonPath, } + (*instances)[r.Type] = append((*instances)[r.Type], inst) // Add all sub resources if they exist. @@ -260,11 +261,12 @@ func (m *manager) DeleteDeployment(name string, forget bool) (*common.Deployment } // Create an empty manifest since resources have been deleted. - err = m.repository.AddManifest(name, &common.Manifest{Deployment: name, Name: generateManifestName()}) - if err != nil { - log.Printf("Failed to add empty manifest") - m.repository.SetDeploymentState(name, failState(err)) - return nil, err + if !forget { + manifest := &common.Manifest{Deployment: name, Name: generateManifestName()} + if err := m.repository.AddManifest(manifest); err != nil { + log.Printf("Failed to add empty manifest") + return nil, err + } } } @@ -274,8 +276,7 @@ func (m *manager) DeleteDeployment(name string, forget bool) (*common.Deployment } // Finally remove the type instances for this deployment. - m.repository.ClearTypeInstances(name) - + m.repository.ClearTypeInstancesForDeployment(name) return d, nil } @@ -301,15 +302,14 @@ func (m *manager) PutDeployment(name string, t *common.Template) (*common.Deploy } manifest.ExpandedConfig = actualConfig - err = m.repository.AddManifest(t.Name, manifest) + err = m.repository.AddManifest(manifest) if err != nil { m.repository.SetDeploymentState(name, failState(err)) return nil, err } // Finally update the type instances for this deployment. - m.addTypeInstances(t.Name, manifest.Name, manifest.Layout) - + m.setTypeInstances(t.Name, manifest.Name, manifest.Layout) return m.repository.GetValidDeployment(t.Name) } @@ -326,11 +326,11 @@ func (m *manager) Expand(t *common.Template) (*common.Manifest, error) { }, nil } -func (m *manager) ListTypes() []string { +func (m *manager) ListTypes() ([]string, error) { return m.repository.ListTypes() } -func (m *manager) ListInstances(typeName string) []*common.TypeInstance { +func (m *manager) ListInstances(typeName string) ([]*common.TypeInstance, error) { return m.repository.GetTypeInstances(typeName) } diff --git a/manager/manager/manager_test.go b/manager/manager/manager_test.go index 0a5066b80..40ebe7f4f 100644 --- a/manager/manager/manager_test.go +++ b/manager/manager/manager_test.go @@ -160,6 +160,7 @@ func newRepositoryStub() *repositoryStub { return ret } +// Deployments. func (repository *repositoryStub) ListDeployments() ([]common.Deployment, error) { if repository.FailListDeployments { return deploymentList, errTest @@ -181,11 +182,6 @@ func (repository *repositoryStub) GetValidDeployment(d string) (*common.Deployme return &deployment, nil } -func (repository *repositoryStub) SetDeploymentState(name string, state *common.DeploymentState) error { - repository.DeploymentStates = append(repository.DeploymentStates, state) - return nil -} - func (repository *repositoryStub) CreateDeployment(d string) (*common.Deployment, error) { repository.Created = append(repository.Created, d) return &deployment, nil @@ -196,22 +192,20 @@ func (repository *repositoryStub) DeleteDeployment(d string, forget bool) (*comm return &deployment, nil } -func (repository *repositoryStub) AddManifest(d string, manifest *common.Manifest) error { - repository.ManifestAdd[d] = manifest +func (repository *repositoryStub) SetDeploymentState(name string, state *common.DeploymentState) error { + repository.DeploymentStates = append(repository.DeploymentStates, state) return nil } -func (repository *repositoryStub) SetManifest(d string, manifest *common.Manifest) error { - repository.ManifestSet[d] = manifest +// Manifests. +func (repository *repositoryStub) AddManifest(manifest *common.Manifest) error { + repository.ManifestAdd[manifest.Deployment] = manifest return nil } -func (repository *repositoryStub) GetLatestManifest(d string) (*common.Manifest, error) { - if d == deploymentName { - return repository.ManifestAdd[d], nil - } - - return nil, errTest +func (repository *repositoryStub) SetManifest(manifest *common.Manifest) error { + repository.ManifestSet[manifest.Deployment] = manifest + return nil } func (repository *repositoryStub) ListManifests(d string) (map[string]*common.Manifest, error) { @@ -230,26 +224,43 @@ func (repository *repositoryStub) GetManifest(d string, m string) (*common.Manif return nil, errTest } -func (repository *repositoryStub) ListTypes() []string { +func (repository *repositoryStub) GetLatestManifest(d string) (*common.Manifest, error) { + if d == deploymentName { + return repository.ManifestAdd[d], nil + } + + return nil, errTest +} + +// Types. +func (repository *repositoryStub) ListTypes() ([]string, error) { repository.ListTypesCalled = true - return []string{} + return []string{}, nil } -func (repository *repositoryStub) GetTypeInstances(t string) []*common.TypeInstance { +func (repository *repositoryStub) GetTypeInstances(t string) ([]*common.TypeInstance, error) { repository.GetTypeInstancesCalled = true - return []*common.TypeInstance{} + return []*common.TypeInstance{}, nil } -func (repository *repositoryStub) ClearTypeInstances(d string) { +func (repository *repositoryStub) ClearTypeInstancesForDeployment(d string) error { repository.TypeInstancesCleared = true + return nil } -func (repository *repositoryStub) SetTypeInstances(d string, is map[string][]*common.TypeInstance) { - for k := range is { - repository.TypeInstances[d] = append(repository.TypeInstances[d], k) +func (repository *repositoryStub) AddTypeInstances(is map[string][]*common.TypeInstance) error { + for t, instances := range is { + for _, instance := range instances { + d := instance.Deployment + repository.TypeInstances[d] = append(repository.TypeInstances[d], t) + } } + + return nil } +func (repository *repositoryStub) Close() {} + var testExpander = &expanderStub{} var testRepository = newRepositoryStub() var testDeployer = newDeployerStub() diff --git a/manager/repository/persistent/persistent.go b/manager/repository/persistent/persistent.go new file mode 100644 index 000000000..fadf1285a --- /dev/null +++ b/manager/repository/persistent/persistent.go @@ -0,0 +1,488 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. +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 persistent implements a persistent deployment repository. +// +// This package is currently implemented using MondoDB, but there is no +// guarantee that it will continue to be implemented using MondoDB in the +// future. +package persistent + +import ( + "fmt" + "log" + "net/url" + "os" + "time" + + "github.com/kubernetes/deployment-manager/common" + "github.com/kubernetes/deployment-manager/manager/repository" + + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +type pDeployment struct { + ID string `bson:"_id"` + common.Deployment +} + +type pManifest struct { + ID string `bson:"_id"` + common.Manifest +} + +type pInstance struct { + ID string `bson:"_id"` + common.TypeInstance +} + +type pRepository struct { + Session *mgo.Session // mongodb session + Deployments *mgo.Collection // deployments collection + Manifests *mgo.Collection // manifests collection + Instances *mgo.Collection // instances collection +} + +// Constants used to configure the MongoDB database. +const ( + DatabaseName = "deployment_manager" + DeploymentsCollectionName = "deployments_collection" + ManifestsCollectionName = "manifests_collection" + InstancesCollectionName = "instances_collection" +) + +// NewRepository returns a new persistent repository. Its lifetime is decopuled +// from the lifetime of the current process. When the process dies, its contents +// will not be affected. +// +// The server argument provides connection information for the repository server. +// It is parsed as a URL, and the username, password, host and port, if provided, +// are used to create the connection string. +func NewRepository(server string) (repository.Repository, error) { + travis := os.Getenv("TRAVIS") + if travis == "true" { + err := fmt.Errorf("cannot use MongoDB in Travis CI due to gopkg.in/mgo.v2 issue #218") + log.Println(err.Error()) + return nil, err + } + + u, err := url.Parse(server) + if err != nil { + err2 := fmt.Errorf("cannot parse url '%s': %s\n", server, err) + log.Println(err2.Error()) + return nil, err2 + } + + u2 := &url.URL{Scheme: "mongodb", User: u.User, Host: u.Host} + server = u2.String() + + session, err := mgo.Dial(server) + if err != nil { + err2 := fmt.Errorf("cannot connect to MongoDB at %s: %s\n", server, err) + log.Println(err2.Error()) + return nil, err2 + } + + session.SetMode(mgo.Strong, false) + session.SetSafe(&mgo.Safe{WMode: "majority"}) + database := session.DB(DatabaseName) + deployments, err := createCollection(database, DeploymentsCollectionName, nil) + if err != nil { + return nil, err + } + + manifests, err := createCollection(database, ManifestsCollectionName, + [][]string{{"manifest.deployment"}}) + if err != nil { + return nil, err + } + + instances, err := createCollection(database, InstancesCollectionName, + [][]string{{"typeinstance.type"}, {"typeinstance.deployment"}}) + if err != nil { + return nil, err + } + + pr := &pRepository{ + Session: session, + Deployments: deployments, + Manifests: manifests, + Instances: instances, + } + + return pr, nil +} + +func createCollection(db *mgo.Database, cName string, keys [][]string) (*mgo.Collection, error) { + c := db.C(cName) + for _, key := range keys { + if err := createIndex(c, key...); err != nil { + return nil, err + } + } + + return c, nil +} + +func createIndex(c *mgo.Collection, key ...string) error { + if err := c.EnsureIndexKey(key...); err != nil { + err2 := fmt.Errorf("cannot create index %v for collection %s: %s\n", key, c.Name, err) + log.Println(err2.Error()) + return err2 + } + + return nil +} + +// Reset returns the repository to its initial state. +func (r *pRepository) Reset() error { + database := r.Session.DB(DatabaseName) + if err := database.DropDatabase(); err != nil { + return fmt.Errorf("cannot drop database %s", database.Name) + } + + r.Close() + return nil +} + +// Close cleans up any resources used by the repository. +func (r *pRepository) Close() { + r.Session.Close() +} + +// ListDeployments returns of all of the deployments in the repository. +func (r *pRepository) ListDeployments() ([]common.Deployment, error) { + var result []pDeployment + if err := r.Deployments.Find(nil).All(&result); err != nil { + return nil, fmt.Errorf("cannot list deployments: %s", err) + } + + deployments := []common.Deployment{} + for _, pd := range result { + deployments = append(deployments, pd.Deployment) + } + + return deployments, nil +} + +// GetDeployment returns the deployment with the supplied name. +// If the deployment is not found, it returns an error. +func (r *pRepository) GetDeployment(name string) (*common.Deployment, error) { + result := pDeployment{} + if err := r.Deployments.FindId(name).One(&result); err != nil { + return nil, fmt.Errorf("cannot get deployment %s: %s", name, err) + } + + return &result.Deployment, nil +} + +// GetValidDeployment returns the deployment with the supplied name. +// If the deployment is not found or marked as deleted, it returns an error. +func (r *pRepository) GetValidDeployment(name string) (*common.Deployment, error) { + d, err := r.GetDeployment(name) + if err != nil { + return nil, err + } + + if d.State.Status == common.DeletedStatus { + return nil, fmt.Errorf("deployment %s is deleted", name) + } + + return d, nil +} + +// CreateDeployment creates a new deployment and stores it in the repository. +func (r *pRepository) CreateDeployment(name string) (*common.Deployment, error) { + exists, _ := r.GetValidDeployment(name) + if exists != nil { + return nil, fmt.Errorf("deployment %s already exists", name) + } + + d := common.NewDeployment(name) + if err := r.insertDeployment(d); err != nil { + return nil, err + } + + log.Printf("created deployment: %v", d) + return d, nil +} + +// SetDeploymentStatus sets the DeploymentStatus of the deployment and updates ModifiedAt +func (r *pRepository) SetDeploymentState(name string, state *common.DeploymentState) error { + d, err := r.GetValidDeployment(name) + if err != nil { + return err + } + + d.State = state + return r.updateDeployment(d) +} + +func (r *pRepository) AddManifest(manifest *common.Manifest) error { + deploymentName := manifest.Deployment + d, err := r.GetValidDeployment(deploymentName) + if err != nil { + return err + } + + count, err := r.Manifests.FindId(manifest.Name).Count() + if err != nil { + return fmt.Errorf("cannot search for manifest %s: %s", manifest.Name, err) + } + + if count > 0 { + return fmt.Errorf("manifest %s already exists", manifest.Name) + } + + if err := r.insertManifest(manifest); err != nil { + return err + } + + d.LatestManifest = manifest.Name + if err := r.updateDeployment(d); err != nil { + return err + } + + log.Printf("Added manifest %s to deployment: %s", manifest.Name, deploymentName) + return nil +} + +// DeleteDeployment deletes the deployment with the supplied name. +// If forget is true, then the deployment is removed from the repository. +// Otherwise, it is marked as deleted and retained. +func (r *pRepository) DeleteDeployment(name string, forget bool) (*common.Deployment, error) { + d, err := r.GetValidDeployment(name) + if err != nil { + return nil, err + } + + if !forget { + d.DeletedAt = time.Now() + d.State = &common.DeploymentState{Status: common.DeletedStatus} + if err := r.updateDeployment(d); err != nil { + return nil, err + } + } else { + d.LatestManifest = "" + if err := r.removeManifestsForDeployment(d); err != nil { + return nil, err + } + + if err := r.removeDeployment(d); err != nil { + return nil, err + } + } + + log.Printf("deleted deployment: %v", d) + return d, nil +} + +func (r *pRepository) insertDeployment(d *common.Deployment) error { + if d != nil && d.Name != "" { + wrapper := pDeployment{ID: d.Name, Deployment: *d} + if err := r.Deployments.Insert(&wrapper); err != nil { + return fmt.Errorf("cannot insert deployment %v: %s", wrapper, err) + } + } + + return nil +} + +func (r *pRepository) removeDeployment(d *common.Deployment) error { + if d != nil && d.Name != "" { + if err := r.Deployments.RemoveId(d.Name); err != nil { + return fmt.Errorf("cannot remove deployment %s: %s", d.Name, err) + } + } + + return nil +} + +func (r *pRepository) updateDeployment(d *common.Deployment) error { + if d != nil && d.Name != "" { + if d.State.Status != common.DeletedStatus { + d.ModifiedAt = time.Now() + } + + wrapper := pDeployment{ID: d.Name, Deployment: *d} + if err := r.Deployments.UpdateId(d.Name, &wrapper); err != nil { + return fmt.Errorf("cannot update deployment %v: %s", wrapper, err) + } + } + + return nil +} + +func (r *pRepository) ListManifests(deploymentName string) (map[string]*common.Manifest, error) { + _, err := r.GetValidDeployment(deploymentName) + if err != nil { + return nil, err + } + + return r.listManifestsForDeployment(deploymentName) +} + +func (r *pRepository) GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) { + _, err := r.GetValidDeployment(deploymentName) + if err != nil { + return nil, err + } + + return r.getManifestForDeployment(deploymentName, manifestName) +} + +// GetLatestManifest returns the latest manifest for a given deployment, +// which by definition is the manifest with the largest time stamp. +func (r *pRepository) GetLatestManifest(deploymentName string) (*common.Manifest, error) { + d, err := r.GetValidDeployment(deploymentName) + if err != nil { + return nil, err + } + + if d.LatestManifest == "" { + return nil, nil + } + + return r.getManifestForDeployment(deploymentName, d.LatestManifest) +} + +// SetManifest sets an existing manifest in the repository to provided manifest. +func (r *pRepository) SetManifest(manifest *common.Manifest) error { + _, err := r.GetManifest(manifest.Deployment, manifest.Name) + if err != nil { + return err + } + + return r.updateManifest(manifest) +} + +func (r *pRepository) updateManifest(m *common.Manifest) error { + if m != nil && m.Name != "" { + wrapper := pManifest{ID: m.Name, Manifest: *m} + if err := r.Manifests.UpdateId(m.Name, &wrapper); err != nil { + return fmt.Errorf("cannot update manifest %v: %s", wrapper, err) + } + } + + return nil +} + +func (r *pRepository) listManifestsForDeployment(deploymentName string) (map[string]*common.Manifest, error) { + query := bson.M{"manifest.deployment": deploymentName} + var result []pManifest + if err := r.Manifests.Find(query).All(&result); err != nil { + return nil, fmt.Errorf("cannot list manifests for deployment %s: %s", deploymentName, err) + } + + l := make(map[string]*common.Manifest, 0) + for _, pm := range result { + l[pm.Name] = &pm.Manifest + } + + return l, nil +} + +func (r *pRepository) getManifestForDeployment(deploymentName string, manifestName string) (*common.Manifest, error) { + result := pManifest{} + if err := r.Manifests.FindId(manifestName).One(&result); err != nil { + return nil, fmt.Errorf("cannot get manifest %s: %s", manifestName, err) + } + + if result.Deployment != deploymentName { + return nil, fmt.Errorf("manifest %s not found in deployment %s", manifestName, deploymentName) + } + + return &result.Manifest, nil +} + +func (r *pRepository) insertManifest(m *common.Manifest) error { + if m != nil && m.Name != "" { + wrapper := pManifest{ID: m.Name, Manifest: *m} + if err := r.Manifests.Insert(&wrapper); err != nil { + return fmt.Errorf("cannot insert manifest %v: %s", wrapper, err) + } + } + + return nil +} + +func (r *pRepository) removeManifestsForDeployment(d *common.Deployment) error { + if d != nil && d.Name != "" { + query := bson.M{"manifest.deployment": d.Name} + if _, err := r.Manifests.RemoveAll(query); err != nil { + return fmt.Errorf("cannot remove all manifests for deployment %s: %s", d.Name, err) + } + } + + return nil +} + +// ListTypes returns all types known from existing instances. +func (r *pRepository) ListTypes() ([]string, error) { + var result []string + if err := r.Instances.Find(nil).Distinct("typeinstance.type", &result); err != nil { + return nil, fmt.Errorf("cannot list type instances: %s", err) + } + + return result, nil +} + +// GetTypeInstances returns all instances of a given type. If typeName is empty +// or equal to "all", returns all instances of all types. +func (r *pRepository) GetTypeInstances(typeName string) ([]*common.TypeInstance, error) { + query := bson.M{"typeinstance.type": typeName} + if typeName == "" || typeName == "all" { + query = nil + } + + var result []pInstance + if err := r.Instances.Find(query).All(&result); err != nil { + return nil, fmt.Errorf("cannot get instances of type %s: %s", typeName, err) + } + + instances := []*common.TypeInstance{} + for _, pi := range result { + instances = append(instances, &pi.TypeInstance) + } + + return instances, nil +} + +// ClearTypeInstancesForDeployment deletes all type instances associated with the given +// deployment from the repository. +func (r *pRepository) ClearTypeInstancesForDeployment(deploymentName string) error { + if deploymentName != "" { + query := bson.M{"typeinstance.deployment": deploymentName} + if _, err := r.Instances.RemoveAll(query); err != nil { + return fmt.Errorf("cannot clear type instances for deployment %s: %s", deploymentName, err) + } + } + + return nil +} + +// AddTypeInstances adds the supplied type instances to the repository. +func (r *pRepository) AddTypeInstances(instances map[string][]*common.TypeInstance) error { + for _, is := range instances { + for _, i := range is { + key := fmt.Sprintf("%s.%s.%s", i.Deployment, i.Type, i.Name) + wrapper := pInstance{ID: key, TypeInstance: *i} + if err := r.Instances.Insert(&wrapper); err != nil { + return fmt.Errorf("cannot insert type instance %v: %s", wrapper, err) + } + } + } + + return nil +} diff --git a/manager/repository/persistent/persistent_test.go b/manager/repository/persistent/persistent_test.go new file mode 100644 index 000000000..47038c76e --- /dev/null +++ b/manager/repository/persistent/persistent_test.go @@ -0,0 +1,110 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. +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 persistent + +import ( + "github.com/kubernetes/deployment-manager/manager/repository" + + "sync" + "testing" +) + +var tryRepository = true +var repositoryLock sync.RWMutex + +func createRepository() repository.Repository { + repositoryLock.Lock() + defer repositoryLock.Unlock() + + if tryRepository { + r, err := NewRepository("mongodb://localhost") + if err == nil { + return r + } + } + + tryRepository = false + return nil +} + +func resetRepository(t *testing.T, r repository.Repository) { + if r != nil { + if err := r.(*pRepository).Reset(); err != nil { + t.Fatalf("cannot reset repository: %s\n", err) + } + } +} + +func TestRepositoryListEmpty(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryListEmpty(t, r) + } +} + +func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryGetFailsWithNonExistentDeployment(t, r) + } +} + +func TestRepositoryCreateDeploymentWorks(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryCreateDeploymentWorks(t, r) + } +} + +func TestRepositoryMultipleManifestsWorks(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryMultipleManifestsWorks(t, r) + } +} + +func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryDeleteFailsWithNonExistentDeployment(t, r) + } +} + +func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryDeleteWorksWithNoLatestManifest(t, r) + } +} + +func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryDeleteDeploymentWorksNoForget(t, r) + } +} + +func TestRepositoryDeleteDeploymentWorksForget(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryDeleteDeploymentWorksForget(t, r) + } +} + +func TestRepositoryTypeInstances(t *testing.T) { + if r := createRepository(); r != nil { + defer resetRepository(t, r) + repository.TestRepositoryTypeInstances(t, r) + } +} diff --git a/manager/repository/repository.go b/manager/repository/repository.go index 8a5385fd1..d4e70d05b 100644 --- a/manager/repository/repository.go +++ b/manager/repository/repository.go @@ -6,7 +6,7 @@ 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. @@ -14,17 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package repository implements a deployment repository using a map. -// It can be easily replaced by a deployment repository that uses some -// form of persistent storage. +// Package repository defines a deployment repository. package repository import ( - "fmt" - "log" - "sync" - "time" - "github.com/kubernetes/deployment-manager/common" ) @@ -40,319 +33,17 @@ type Repository interface { SetDeploymentState(name string, state *common.DeploymentState) error // Manifests. - AddManifest(deploymentName string, manifest *common.Manifest) error - SetManifest(deploymentName string, manifest *common.Manifest) error + AddManifest(manifest *common.Manifest) error + SetManifest(manifest *common.Manifest) error ListManifests(deploymentName string) (map[string]*common.Manifest, error) GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) GetLatestManifest(deploymentName string) (*common.Manifest, error) // Types. - ListTypes() []string - GetTypeInstances(typeName string) []*common.TypeInstance - ClearTypeInstances(deploymentName string) - SetTypeInstances(deploymentName string, instances map[string][]*common.TypeInstance) -} - -// deploymentTypeInstanceMap stores type instances mapped by deployment name. -// This allows for simple updating and deleting of per-deployment instances -// when deployments are created/updated/deleted. -type deploymentTypeInstanceMap map[string][]*common.TypeInstance -type typeInstanceMap map[string]deploymentTypeInstanceMap - -type mapBasedRepository struct { - sync.RWMutex - deployments map[string]common.Deployment - manifests map[string]map[string]*common.Manifest - instances typeInstanceMap -} - -// NewMapBasedRepository returns a new map based repository. -func NewMapBasedRepository() Repository { - return &mapBasedRepository{ - deployments: make(map[string]common.Deployment, 0), - manifests: make(map[string]map[string]*common.Manifest, 0), - instances: typeInstanceMap{}, - } -} - -// ListDeployments returns of all of the deployments in the repository. -func (r *mapBasedRepository) ListDeployments() ([]common.Deployment, error) { - r.RLock() - defer r.RUnlock() - - l := []common.Deployment{} - for _, deployment := range r.deployments { - l = append(l, deployment) - } - - return l, nil -} - -// GetDeployment returns the deployment with the supplied name. -// If the deployment is not found, it returns an error. -func (r *mapBasedRepository) GetDeployment(name string) (*common.Deployment, error) { - d, ok := r.deployments[name] - if !ok { - return nil, fmt.Errorf("deployment %s not found", name) - } - return &d, nil -} - -// GetValidDeployment returns the deployment with the supplied name. -// If the deployment is not found or marked as deleted, it returns an error. -func (r *mapBasedRepository) GetValidDeployment(name string) (*common.Deployment, error) { - d, err := r.GetDeployment(name) - if err != nil { - return nil, err - } - - if d.State.Status == common.DeletedStatus { - return nil, fmt.Errorf("deployment %s is deleted", name) - } - - return d, nil -} - -// SetDeploymentState sets the DeploymentState of the deployment and updates ModifiedAt -func (r *mapBasedRepository) SetDeploymentState(name string, state *common.DeploymentState) error { - return func() error { - r.Lock() - defer r.Unlock() - - d, err := r.GetValidDeployment(name) - if err != nil { - return err - } - - d.State = state - d.ModifiedAt = time.Now() - r.deployments[name] = *d - return nil - }() -} - -// CreateDeployment creates a new deployment and stores it in the repository. -func (r *mapBasedRepository) CreateDeployment(name string) (*common.Deployment, error) { - d, err := func() (*common.Deployment, error) { - r.Lock() - defer r.Unlock() - - exists, _ := r.GetValidDeployment(name) - if exists != nil { - return nil, fmt.Errorf("Deployment %s already exists", name) - } - - d := common.NewDeployment(name) - d.DeployedAt = time.Now() - r.deployments[name] = *d - return d, nil - }() - - if err != nil { - return nil, err - } - - log.Printf("created deployment: %v", d) - return d, nil -} - -// AddManifest adds a manifest to the repository and repoints the latest -// manifest to it for the corresponding deployment. -func (r *mapBasedRepository) AddManifest(deploymentName string, manifest *common.Manifest) error { - r.Lock() - defer r.Unlock() - - l, err := r.listManifestsForDeployment(deploymentName) - if err != nil { - return err - } - - // Make sure the manifest doesn't already exist, and if not, add the manifest to - // map of manifests this deployment has - if _, ok := l[manifest.Name]; ok { - return fmt.Errorf("Manifest %s already exists in deployment %s", manifest.Name, deploymentName) - } - - d, err := r.GetValidDeployment(deploymentName) - if err != nil { - return err - } - - l[manifest.Name] = manifest - d.LatestManifest = manifest.Name - r.deployments[deploymentName] = *d - - log.Printf("Added manifest %s to deployment: %s", manifest.Name, deploymentName) - return nil -} - -// SetManifest sets an existing manifest in the repository to provided -// manifest. -func (r *mapBasedRepository) SetManifest(deploymentName string, manifest *common.Manifest) error { - r.Lock() - defer r.Unlock() - - l, err := r.listManifestsForDeployment(deploymentName) - if err != nil { - return err - } - - l[manifest.Name] = manifest - return nil -} - -// DeleteDeployment deletes the deployment with the supplied name. -// If forget is true, then the deployment is removed from the repository. -// Otherwise, it is marked as deleted and retained. -func (r *mapBasedRepository) DeleteDeployment(name string, forget bool) (*common.Deployment, error) { - d, err := func() (*common.Deployment, error) { - r.Lock() - defer r.Unlock() - - d, err := r.GetValidDeployment(name) - if err != nil { - return nil, err - } - - if !forget { - d.DeletedAt = time.Now() - d.State = &common.DeploymentState{Status: common.DeletedStatus} - r.deployments[name] = *d - } else { - delete(r.deployments, name) - delete(r.manifests, name) - d.LatestManifest = "" - } - - return d, nil - }() - - if err != nil { - return nil, err - } - - log.Printf("deleted deployment: %v", d) - return d, nil -} - -func (r *mapBasedRepository) ListManifests(deploymentName string) (map[string]*common.Manifest, error) { - r.Lock() - defer r.Unlock() - - _, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - return r.listManifestsForDeployment(deploymentName) -} - -func (r *mapBasedRepository) listManifestsForDeployment(deploymentName string) (map[string]*common.Manifest, error) { - l, ok := r.manifests[deploymentName] - if !ok { - l = make(map[string]*common.Manifest, 0) - r.manifests[deploymentName] = l - } - - return l, nil -} - -func (r *mapBasedRepository) GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) { - r.Lock() - defer r.Unlock() - - _, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - return r.getManifestForDeployment(deploymentName, manifestName) -} - -func (r *mapBasedRepository) getManifestForDeployment(deploymentName string, manifestName string) (*common.Manifest, error) { - l, err := r.listManifestsForDeployment(deploymentName) - if err != nil { - return nil, err - } - - m, ok := l[manifestName] - if !ok { - return nil, fmt.Errorf("manifest %s not found in deployment %s", manifestName, deploymentName) - } - - return m, nil -} - -// GetLatestManifest returns the latest manifest for a given deployment, -// which by definition is the manifest with the largest time stamp. -func (r *mapBasedRepository) GetLatestManifest(deploymentName string) (*common.Manifest, error) { - r.Lock() - defer r.Unlock() - - d, err := r.GetValidDeployment(deploymentName) - if err != nil { - return nil, err - } - - return r.getManifestForDeployment(deploymentName, d.LatestManifest) -} - -// ListTypes returns all types known from existing instances. -func (r *mapBasedRepository) ListTypes() []string { - var keys []string - for k := range r.instances { - keys = append(keys, k) - } - - return keys -} - -// GetTypeInstances returns all instances of a given type. If type is empty, -// returns all instances for all types. -func (r *mapBasedRepository) GetTypeInstances(typeName string) []*common.TypeInstance { - r.Lock() - defer r.Unlock() - - var instances []*common.TypeInstance - for t, dInstMap := range r.instances { - if t == typeName || typeName == "all" { - for _, i := range dInstMap { - instances = append(instances, i...) - } - } - } - - return instances -} - -// ClearTypeInstances deletes all instances associated with the given -// deployment name from the type instance repository. -func (r *mapBasedRepository) ClearTypeInstances(deploymentName string) { - r.Lock() - defer r.Unlock() - - for t, dMap := range r.instances { - delete(dMap, deploymentName) - if len(dMap) == 0 { - delete(r.instances, t) - } - } -} - -// SetTypeInstances sets all type instances for a given deployment name. -// -// To clear the current set of instances first, caller should first use -// ClearTypeInstances(). -func (r *mapBasedRepository) SetTypeInstances(deploymentName string, instances map[string][]*common.TypeInstance) { - r.Lock() - defer r.Unlock() - - // Add each instance list to the appropriate type map. - for t, is := range instances { - if r.instances[t] == nil { - r.instances[t] = make(deploymentTypeInstanceMap) - } + ListTypes() ([]string, error) + GetTypeInstances(typeName string) ([]*common.TypeInstance, error) + ClearTypeInstancesForDeployment(deploymentName string) error + AddTypeInstances(instances map[string][]*common.TypeInstance) error - r.instances[t][deploymentName] = is - } + Close() } diff --git a/manager/repository/repository_test.go b/manager/repository/test_common.go similarity index 67% rename from manager/repository/repository_test.go rename to manager/repository/test_common.go index 43dfa8ea5..3d5586172 100644 --- a/manager/repository/repository_test.go +++ b/manager/repository/test_common.go @@ -6,7 +6,7 @@ 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. @@ -23,31 +23,28 @@ import ( "testing" ) -func TestRepositoryListEmpty(t *testing.T) { - r := NewMapBasedRepository() +// TestRepositoryListEmpty checks that listing an empty repository works. +func TestRepositoryListEmpty(t *testing.T, r Repository) { d, err := r.ListDeployments() if err != nil { t.Fatal("List Deployments failed") } + if len(d) != 0 { t.Fatal("Returned non zero list") } } -func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T) { - r := NewMapBasedRepository() +// TestRepositoryGetFailsWithNonExistentDeployment checks that getting a non-existent deployment fails. +func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T, r Repository) { _, err := r.GetDeployment("nothere") if err == nil { t.Fatal("GetDeployment didn't fail with non-existent deployment") } - if err.Error() != "deployment nothere not found" { - t.Fatal("Error message doesn't match") - } } -func testCreateDeploymentWithManifests(t *testing.T, count int) { +func testCreateDeploymentWithManifests(t *testing.T, r Repository, count int) { var deploymentName = "mydeployment" - r := NewMapBasedRepository() d, err := r.CreateDeployment(deploymentName) if err != nil { @@ -58,14 +55,16 @@ func testCreateDeploymentWithManifests(t *testing.T, count int) { if err != nil { t.Fatalf("ListDeployments failed: %v", err) } + if len(l) != 1 { - t.Fatalf("List of deployments is not 1: %d", len(l)) + t.Fatalf("Number of deployments listed is not 1: %d", len(l)) } dNew, err := r.GetDeployment(deploymentName) if err != nil { t.Fatalf("GetDeployment failed: %v", err) } + if dNew.Name != d.Name { t.Fatalf("Deployment Names don't match, got: %v, expected %v", dNew, d) } @@ -74,6 +73,7 @@ func testCreateDeploymentWithManifests(t *testing.T, count int) { if err != nil { t.Fatalf("ListManifests failed: %v", err) } + if len(mList) != 0 { t.Fatalf("Deployment has non-zero manifest count: %d", len(mList)) } @@ -81,10 +81,11 @@ func testCreateDeploymentWithManifests(t *testing.T, count int) { for i := 0; i < count; i++ { var manifestName = fmt.Sprintf("manifest-%d", i) manifest := common.Manifest{Deployment: deploymentName, Name: manifestName} - err := r.AddManifest(deploymentName, &manifest) + err := r.AddManifest(&manifest) if err != nil { t.Fatalf("AddManifest failed: %v", err) } + d, err = r.GetDeployment(deploymentName) if err != nil { t.Fatalf("GetDeployment failed: %v", err) @@ -98,6 +99,7 @@ func testCreateDeploymentWithManifests(t *testing.T, count int) { if err != nil { t.Fatalf("ListManifests failed: %v", err) } + if len(mListNew) != i+1 { t.Fatalf("Deployment has unexpected manifest count: want %d, have %d", i+1, len(mListNew)) } @@ -106,98 +108,110 @@ func testCreateDeploymentWithManifests(t *testing.T, count int) { if err != nil { t.Fatalf("GetManifest failed: %v", err) } + if m.Name != manifestName { t.Fatalf("Unexpected manifest name: want %s, have %s", manifestName, m.Name) } } } -func TestRepositoryCreateDeploymentWorks(t *testing.T) { - testCreateDeploymentWithManifests(t, 1) +// TestRepositoryCreateDeploymentWorks checks that creating a deployment works. +func TestRepositoryCreateDeploymentWorks(t *testing.T, r Repository) { + testCreateDeploymentWithManifests(t, r, 1) } -func TestRepositoryMultipleManifestsWorks(t *testing.T) { - testCreateDeploymentWithManifests(t, 7) +// TestRepositoryMultipleManifestsWorks checks that creating a deploymente with multiple manifests works. +func TestRepositoryMultipleManifestsWorks(t *testing.T, r Repository) { + testCreateDeploymentWithManifests(t, r, 7) } -func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T) { +// TestRepositoryDeleteFailsWithNonExistentDeployment checks that deleting a non-existent deployment fails. +func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T, r Repository) { var deploymentName = "mydeployment" - r := NewMapBasedRepository() d, err := r.DeleteDeployment(deploymentName, false) if err == nil { t.Fatalf("DeleteDeployment didn't fail with non existent deployment") } + if d != nil { t.Fatalf("DeleteDeployment returned non-nil for non existent deployment") } } -func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T) { +// TestRepositoryDeleteWorksWithNoLatestManifest checks that deleting a deployment with no latest manifest works. +func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T, r Repository) { var deploymentName = "mydeployment" - r := NewMapBasedRepository() _, err := r.CreateDeployment(deploymentName) if err != nil { t.Fatalf("CreateDeployment failed: %v", err) } + dDeleted, err := r.DeleteDeployment(deploymentName, false) if err != nil { t.Fatalf("DeleteDeployment failed: %v", err) } + if dDeleted.State.Status != common.DeletedStatus { t.Fatalf("Deployment Status is not deleted") } + if _, err := r.ListManifests(deploymentName); err == nil { t.Fatalf("Manifests are not deleted") } } -func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T) { +// TestRepositoryDeleteDeploymentWorksNoForget checks that deleting a deployment without forgetting it works. +func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T, r Repository) { var deploymentName = "mydeployment" var manifestName = "manifest-0" - r := NewMapBasedRepository() manifest := common.Manifest{Deployment: deploymentName, Name: manifestName} _, err := r.CreateDeployment(deploymentName) if err != nil { t.Fatalf("CreateDeployment failed: %v", err) } - err = r.AddManifest(deploymentName, &manifest) + + err = r.AddManifest(&manifest) if err != nil { t.Fatalf("AddManifest failed: %v", err) } + dDeleted, err := r.DeleteDeployment(deploymentName, false) if err != nil { t.Fatalf("DeleteDeployment failed: %v", err) } + if dDeleted.State.Status != common.DeletedStatus { t.Fatalf("Deployment Status is not deleted") } } -func TestRepositoryDeleteDeploymentWorksForget(t *testing.T) { +// TestRepositoryDeleteDeploymentWorksForget checks that deleting and forgetting a deployment works. +func TestRepositoryDeleteDeploymentWorksForget(t *testing.T, r Repository) { var deploymentName = "mydeployment" var manifestName = "manifest-0" - r := NewMapBasedRepository() manifest := common.Manifest{Deployment: deploymentName, Name: manifestName} _, err := r.CreateDeployment(deploymentName) if err != nil { t.Fatalf("CreateDeployment failed: %v", err) } - err = r.AddManifest(deploymentName, &manifest) + + err = r.AddManifest(&manifest) if err != nil { t.Fatalf("AddManifest failed: %v", err) } + dDeleted, err := r.DeleteDeployment(deploymentName, true) if err != nil { t.Fatalf("DeleteDeployment failed: %v", err) } + if dDeleted.State.Status != common.CreatedStatus { t.Fatalf("Deployment Status is not created") } } -func TestRepositoryTypeInstances(t *testing.T) { - r := NewMapBasedRepository() - +// TestRepositoryTypeInstances checks that type instances can be listed and retrieved successfully. +func TestRepositoryTypeInstances(t *testing.T, r Repository) { d1Map := map[string][]*common.TypeInstance{ "t1": []*common.TypeInstance{ &common.TypeInstance{ @@ -234,46 +248,93 @@ func TestRepositoryTypeInstances(t *testing.T) { }, } - if instances := r.GetTypeInstances("noinstances"); len(instances) != 0 { + instances, err := r.GetTypeInstances("noinstances") + if err != nil { + t.Fatal(err) + } + + if len(instances) != 0 { t.Fatalf("expected no instances: %v", instances) } - if types := r.ListTypes(); len(types) != 0 { + types, err := r.ListTypes() + if err != nil { + t.Fatal(err) + } + + if len(types) != 0 { t.Fatalf("expected no types: %v", types) } - r.SetTypeInstances("d1", d1Map) - r.SetTypeInstances("d2", d2Map) - r.SetTypeInstances("d3", d3Map) + r.AddTypeInstances(d1Map) + r.AddTypeInstances(d2Map) + r.AddTypeInstances(d3Map) - if instances := r.GetTypeInstances("unknowntype"); len(instances) != 0 { + instances, err = r.GetTypeInstances("unknowntype") + if err != nil { + t.Fatal(err) + } + + if len(instances) != 0 { t.Fatalf("expected no instances: %v", instances) } - if instances := r.GetTypeInstances("t1"); len(instances) != 1 { + instances, err = r.GetTypeInstances("t1") + if err != nil { + t.Fatal(err) + } + + if len(instances) != 1 { t.Fatalf("expected one instance: %v", instances) } - if instances := r.GetTypeInstances("t2"); len(instances) != 2 { + instances, err = r.GetTypeInstances("t2") + if err != nil { + t.Fatal(err) + } + + if len(instances) != 2 { t.Fatalf("expected two instances: %v", instances) } - if instances := r.GetTypeInstances("all"); len(instances) != 3 { + instances, err = r.GetTypeInstances("all") + if err != nil { + t.Fatal(err) + } + + if len(instances) != 3 { t.Fatalf("expected three total instances: %v", instances) } - if types := r.ListTypes(); len(types) != 2 { + types, err = r.ListTypes() + if err != nil { + t.Fatal(err) + } + + if len(types) != 2 { t.Fatalf("expected two total types: %v", types) } - r.ClearTypeInstances("d1") - if instances := r.GetTypeInstances("t1"); len(instances) != 0 { + err = r.ClearTypeInstancesForDeployment("d1") + if err != nil { + t.Fatal(err) + } + + instances, err = r.GetTypeInstances("t1") + if err != nil { + t.Fatal(err) + } + + if len(instances) != 0 { t.Fatalf("expected no instances after clear: %v", instances) } - if types := r.ListTypes(); len(types) != 1 { + types, err = r.ListTypes() + if err != nil { + t.Fatal(err) + } + + if len(types) != 1 { t.Fatalf("expected one total type: %v", types) } } - -// TODO(vaikas): Add more tests diff --git a/manager/repository/transient/transient.go b/manager/repository/transient/transient.go new file mode 100644 index 000000000..36b7176f7 --- /dev/null +++ b/manager/repository/transient/transient.go @@ -0,0 +1,325 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 transient implements a transient deployment repository. +package transient + +import ( + "fmt" + "log" + "sync" + "time" + + "github.com/kubernetes/deployment-manager/common" + "github.com/kubernetes/deployment-manager/manager/repository" +) + +// deploymentTypeInstanceMap stores type instances mapped by deployment name. +// This allows for simple updating and deleting of per-deployment instances +// when deployments are created/updated/deleted. +type deploymentTypeInstanceMap map[string][]*common.TypeInstance + +type tRepository struct { + sync.RWMutex + deployments map[string]common.Deployment + manifests map[string]map[string]*common.Manifest + instances map[string]deploymentTypeInstanceMap +} + +// NewRepository returns a new transient repository. Its lifetime is coupled +// to the lifetime of the current process. When the process dies, its contents +// will be permanently destroyed. +func NewRepository() repository.Repository { + return &tRepository{ + deployments: make(map[string]common.Deployment, 0), + manifests: make(map[string]map[string]*common.Manifest, 0), + instances: make(map[string]deploymentTypeInstanceMap, 0), + } +} + +func (r *tRepository) Close() { + r.deployments = make(map[string]common.Deployment, 0) + r.manifests = make(map[string]map[string]*common.Manifest, 0) + r.instances = make(map[string]deploymentTypeInstanceMap, 0) +} + +// ListDeployments returns of all of the deployments in the repository. +func (r *tRepository) ListDeployments() ([]common.Deployment, error) { + l := []common.Deployment{} + for _, deployment := range r.deployments { + l = append(l, deployment) + } + + return l, nil +} + +// GetDeployment returns the deployment with the supplied name. +// If the deployment is not found, it returns an error. +func (r *tRepository) GetDeployment(name string) (*common.Deployment, error) { + d, ok := r.deployments[name] + if !ok { + return nil, fmt.Errorf("deployment %s not found", name) + } + + return &d, nil +} + +// GetValidDeployment returns the deployment with the supplied name. +// If the deployment is not found or marked as deleted, it returns an error. +func (r *tRepository) GetValidDeployment(name string) (*common.Deployment, error) { + d, err := r.GetDeployment(name) + if err != nil { + return nil, err + } + + if d.State.Status == common.DeletedStatus { + return nil, fmt.Errorf("deployment %s is deleted", name) + } + + return d, nil +} + +// SetDeploymentState sets the DeploymentState of the deployment and updates ModifiedAt +func (r *tRepository) SetDeploymentState(name string, state *common.DeploymentState) error { + r.Lock() + defer r.Unlock() + + d, err := r.GetValidDeployment(name) + if err != nil { + return err + } + + d.State = state + d.ModifiedAt = time.Now() + r.deployments[name] = *d + return nil +} + +// CreateDeployment creates a new deployment and stores it in the repository. +func (r *tRepository) CreateDeployment(name string) (*common.Deployment, error) { + r.Lock() + defer r.Unlock() + + exists, _ := r.GetValidDeployment(name) + if exists != nil { + return nil, fmt.Errorf("Deployment %s already exists", name) + } + + d := common.NewDeployment(name) + r.deployments[name] = *d + + log.Printf("created deployment: %v", d) + return d, nil +} + +// AddManifest adds a manifest to the repository and repoints the latest +// manifest to it for the corresponding deployment. +func (r *tRepository) AddManifest(manifest *common.Manifest) error { + r.Lock() + defer r.Unlock() + + deploymentName := manifest.Deployment + l, err := r.ListManifests(deploymentName) + if err != nil { + return err + } + + // Make sure the manifest doesn't already exist, and if not, add the manifest to + // map of manifests this deployment has + if _, ok := l[manifest.Name]; ok { + return fmt.Errorf("Manifest %s already exists in deployment %s", manifest.Name, deploymentName) + } + + d, err := r.GetValidDeployment(deploymentName) + if err != nil { + return err + } + + l[manifest.Name] = manifest + d.LatestManifest = manifest.Name + d.ModifiedAt = time.Now() + r.deployments[deploymentName] = *d + + log.Printf("Added manifest %s to deployment: %s", manifest.Name, deploymentName) + return nil +} + +// SetManifest sets an existing manifest in the repository to provided manifest. +func (r *tRepository) SetManifest(manifest *common.Manifest) error { + r.Lock() + defer r.Unlock() + + l, err := r.ListManifests(manifest.Deployment) + if err != nil { + return err + } + + if _, ok := l[manifest.Name]; !ok { + return fmt.Errorf("manifest %s not found", manifest.Name) + } + + l[manifest.Name] = manifest + return nil +} + +// DeleteDeployment deletes the deployment with the supplied name. +// If forget is true, then the deployment is removed from the repository. +// Otherwise, it is marked as deleted and retained. +func (r *tRepository) DeleteDeployment(name string, forget bool) (*common.Deployment, error) { + r.Lock() + defer r.Unlock() + + d, err := r.GetValidDeployment(name) + if err != nil { + return nil, err + } + + if !forget { + d.DeletedAt = time.Now() + d.State = &common.DeploymentState{Status: common.DeletedStatus} + r.deployments[name] = *d + } else { + delete(r.deployments, name) + delete(r.manifests, name) + d.LatestManifest = "" + } + + log.Printf("deleted deployment: %v", d) + return d, nil +} + +func (r *tRepository) ListManifests(deploymentName string) (map[string]*common.Manifest, error) { + _, err := r.GetValidDeployment(deploymentName) + if err != nil { + return nil, err + } + + return r.listManifestsForDeployment(deploymentName) +} + +func (r *tRepository) listManifestsForDeployment(deploymentName string) (map[string]*common.Manifest, error) { + l, ok := r.manifests[deploymentName] + if !ok { + l = make(map[string]*common.Manifest, 0) + r.manifests[deploymentName] = l + } + + return l, nil +} + +func (r *tRepository) GetManifest(deploymentName string, manifestName string) (*common.Manifest, error) { + _, err := r.GetValidDeployment(deploymentName) + if err != nil { + return nil, err + } + + return r.getManifestForDeployment(deploymentName, manifestName) +} + +func (r *tRepository) getManifestForDeployment(deploymentName string, manifestName string) (*common.Manifest, error) { + l, err := r.listManifestsForDeployment(deploymentName) + if err != nil { + return nil, err + } + + m, ok := l[manifestName] + if !ok { + return nil, fmt.Errorf("manifest %s not found in deployment %s", manifestName, deploymentName) + } + + return m, nil +} + +// GetLatestManifest returns the latest manifest for a given deployment, +// which by definition is the manifest with the largest time stamp. +func (r *tRepository) GetLatestManifest(deploymentName string) (*common.Manifest, error) { + d, err := r.GetValidDeployment(deploymentName) + if err != nil { + return nil, err + } + + if d.LatestManifest == "" { + return nil, nil + } + + return r.getManifestForDeployment(deploymentName, d.LatestManifest) +} + +// ListTypes returns all types known from existing instances. +func (r *tRepository) ListTypes() ([]string, error) { + var keys []string + for k := range r.instances { + keys = append(keys, k) + } + + return keys, nil +} + +// GetTypeInstances returns all instances of a given type. If type is empty, +// returns all instances for all types. +func (r *tRepository) GetTypeInstances(typeName string) ([]*common.TypeInstance, error) { + var instances []*common.TypeInstance + for t, dInstMap := range r.instances { + if t == typeName || typeName == "" || typeName == "all" { + for _, i := range dInstMap { + instances = append(instances, i...) + } + } + } + + return instances, nil +} + +// ClearTypeInstancesForDeployment deletes all type instances associated with the given +// deployment from the repository. +func (r *tRepository) ClearTypeInstancesForDeployment(deploymentName string) error { + r.Lock() + defer r.Unlock() + + for t, dMap := range r.instances { + delete(dMap, deploymentName) + if len(dMap) == 0 { + delete(r.instances, t) + } + } + + return nil +} + +// AddTypeInstances adds the supplied type instances to the repository. +func (r *tRepository) AddTypeInstances(instances map[string][]*common.TypeInstance) error { + r.Lock() + defer r.Unlock() + + // Add instances to the appropriate type and deployment maps. + for t, is := range instances { + if r.instances[t] == nil { + r.instances[t] = make(deploymentTypeInstanceMap) + } + + tmap := r.instances[t] + for _, instance := range is { + deployment := instance.Deployment + if tmap[deployment] == nil { + tmap[deployment] = make([]*common.TypeInstance, 0) + } + + tmap[deployment] = append(tmap[deployment], instance) + } + } + + return nil +} diff --git a/manager/repository/transient/transient_test.go b/manager/repository/transient/transient_test.go new file mode 100644 index 000000000..368898eb2 --- /dev/null +++ b/manager/repository/transient/transient_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. +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 transient + +import ( + "github.com/kubernetes/deployment-manager/manager/repository" + "testing" +) + +func TestRepositoryListEmpty(t *testing.T) { + repository.TestRepositoryListEmpty(t, NewRepository()) +} + +func TestRepositoryGetFailsWithNonExistentDeployment(t *testing.T) { + repository.TestRepositoryGetFailsWithNonExistentDeployment(t, NewRepository()) +} + +func TestRepositoryCreateDeploymentWorks(t *testing.T) { + repository.TestRepositoryCreateDeploymentWorks(t, NewRepository()) +} + +func TestRepositoryMultipleManifestsWorks(t *testing.T) { + repository.TestRepositoryMultipleManifestsWorks(t, NewRepository()) +} + +func TestRepositoryDeleteFailsWithNonExistentDeployment(t *testing.T) { + repository.TestRepositoryDeleteFailsWithNonExistentDeployment(t, NewRepository()) +} + +func TestRepositoryDeleteWorksWithNoLatestManifest(t *testing.T) { + repository.TestRepositoryDeleteWorksWithNoLatestManifest(t, NewRepository()) +} + +func TestRepositoryDeleteDeploymentWorksNoForget(t *testing.T) { + repository.TestRepositoryDeleteDeploymentWorksNoForget(t, NewRepository()) +} + +func TestRepositoryDeleteDeploymentWorksForget(t *testing.T) { + repository.TestRepositoryDeleteDeploymentWorksForget(t, NewRepository()) +} + +func TestRepositoryTypeInstances(t *testing.T) { + repository.TestRepositoryTypeInstances(t, NewRepository()) +}