diff --git a/daemon/inertiad/build/builder.go b/daemon/inertiad/build/builder.go index a69eed95..b1b2649c 100644 --- a/daemon/inertiad/build/builder.go +++ b/daemon/inertiad/build/builder.go @@ -280,8 +280,7 @@ func (b *Builder) dockerBuild(d Config, cli *docker.Client, return func() error { return b.run(ctx, cli, d.Name, containerResp.ID, out) }, nil } -// run starts project and tracks all active project containers and pipes an error -// to the returned channel if any container exits or errors. +// run starts project and tracks all active project containers func (b *Builder) run(ctx context.Context, client *docker.Client, name, id string, out io.Writer) error { reportProjectStartup(name, out) return client.ContainerStart(ctx, id, types.ContainerStartOptions{}) diff --git a/daemon/inertiad/daemon/up.go b/daemon/inertiad/daemon/up.go index a526d5d8..4d136de9 100644 --- a/daemon/inertiad/daemon/up.go +++ b/daemon/inertiad/daemon/up.go @@ -97,5 +97,11 @@ func (s *Server) upHandler(w http.ResponseWriter, r *http.Request) { return } + // Update container management history following a successful build and deployment + err = s.deployment.UpdateContainerHistory(s.docker) + if err != nil { + stream.Error(res.ErrInternalServer("failed to update container history following build", err)) + } + stream.Success(res.Msg("Project startup initiated!", http.StatusCreated)) } diff --git a/daemon/inertiad/project/data.go b/daemon/inertiad/project/data.go index 16545ad8..4c76c67d 100644 --- a/daemon/inertiad/project/data.go +++ b/daemon/inertiad/project/data.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "os" + "time" "github.com/ubclaunchpad/inertia/daemon/inertiad/crypto" bolt "go.etcd.io/bbolt" @@ -14,7 +15,8 @@ import ( var ( // database buckets - envVariableBucket = []byte("envVariables") + envVariableBucket = []byte("envVariables") + deployedProjectsBucket = []byte("deployedProjects") ) // DeploymentDataManager stores persistent deployment configuration @@ -50,6 +52,14 @@ func NewDataManager(dbPath string, keyPath string) (*DeploymentDataManager, erro } if err = db.Update(func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists(envVariableBucket) + if err != nil { + return fmt.Errorf("failed to created env variable bucket: %s", err.Error()) + } + + _, err = tx.CreateBucketIfNotExists(deployedProjectsBucket) + if err != nil { + return fmt.Errorf("failed to created deployed projects bucket: %s", err.Error()) + } return err }); err != nil { return nil, fmt.Errorf("failed to instantiate database: %s", err.Error()) @@ -63,8 +73,7 @@ func NewDataManager(dbPath string, keyPath string) (*DeploymentDataManager, erro // AddEnvVariable adds a new environment variable that will be applied // to all project containers -func (c *DeploymentDataManager) AddEnvVariable(name, value string, - encrypt bool) error { +func (c *DeploymentDataManager) AddEnvVariable(name, value string, encrypt bool) error { if len(name) == 0 || len(value) == 0 { return errors.New("invalid env configuration") } @@ -138,6 +147,64 @@ func (c *DeploymentDataManager) GetEnvVariables(decrypt bool) ([]string, error) return envs, err } +// AddProjectBuildData stores and tracks metadata from successful builds +// TODO: Change name, error check, only insert project mdata inside private helper 'update build' +func (c *DeploymentDataManager) AddProjectBuildData(projectName string, mdata DeploymentMetadata) error { + // encode metadata so it can be stored as byte array + encodedMdata, err := json.Marshal(mdata) + if err != nil { + return fmt.Errorf("failure encrypting metadata: %s", err.Error()) + } + err = c.db.Update(func(tx *bolt.Tx) error { + depProjectsBkt := tx.Bucket(deployedProjectsBucket) + // if bkt with project name doesnt exist create new bkt, otherwise update existing bucket + if projectBkt := depProjectsBkt.Bucket([]byte(projectName)); projectBkt == nil { + projectBkt, err := depProjectsBkt.CreateBucket([]byte(projectName)) + if err != nil { + return fmt.Errorf("failure creating project bkt: %s", err.Error()) + } + + if err := projectBkt.Put([]byte(time.Now().String()), encodedMdata); err != nil { + return fmt.Errorf("failure inserting project metadata: %s", err.Error()) + } + } + return nil + }) + return c.UpdateProjectBuildData(projectName, mdata) +} + +// UpdateProjectBuildData updates existing project bkt with recent build's metadata +func (c *DeploymentDataManager) UpdateProjectBuildData(projectName string, + mdata DeploymentMetadata) error { + // encode metadata so it can be stored as byte array + encodedMdata, err := json.Marshal(mdata) + if err != nil { + return fmt.Errorf("failure encrypting metadata: %s", err.Error()) + } + return c.db.Update(func(tx *bolt.Tx) error { + depProjectBkt := tx.Bucket(deployedProjectsBucket) + projectBkt := depProjectBkt.Bucket([]byte(projectName)) + + if err := projectBkt.Put([]byte(time.Now().String()), encodedMdata); err != nil { + return fmt.Errorf("failure updating db with project metadata: %s", err.Error()) + } + return nil + }) + +} + +// GetNumOfDeployedProjects returns number of projects currently deployed +func (c *DeploymentDataManager) GetNumOfDeployedProjects(projectName string) (int, error) { + var numBkts int + err := c.db.View(func(tx *bolt.Tx) error { + depProjectBkt := tx.Bucket(deployedProjectsBucket) + bktStats := depProjectBkt.Stats() + numBkts = bktStats.BucketN + return nil + }) + return numBkts, err +} + func (c *DeploymentDataManager) destroy() error { return c.db.Update(func(tx *bolt.Tx) error { if err := tx.DeleteBucket(envVariableBucket); err != nil { diff --git a/daemon/inertiad/project/data_test.go b/daemon/inertiad/project/data_test.go index 718c7372..46688842 100644 --- a/daemon/inertiad/project/data_test.go +++ b/daemon/inertiad/project/data_test.go @@ -65,6 +65,49 @@ func TestDataManager_EnvVariableOperations(t *testing.T) { } } +func TestDataManager_ProjectBuildDataOperations(t *testing.T) { + type args struct { + projectName string + metadata DeploymentMetadata + numProjects int + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"valid project build", args{"projectB", DeploymentMetadata{"hash", "ID", "status", "time"}, 2}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := "./test_config" + err := os.Mkdir(dir, os.ModePerm) + assert.Nil(t, err) + defer os.RemoveAll(dir) + + // Instantiate + c, err := NewDataManager(path.Join(dir, "deployment.db"), path.Join(dir, "key")) + assert.Nil(t, err) + + // Add + err = c.AddProjectBuildData(tt.args.projectName, tt.args.metadata) + assert.Equal(t, tt.wantErr, (err != nil)) + + // Adding using same project name should only update existing bucket + err = c.AddProjectBuildData(tt.args.projectName, tt.args.metadata) + numBkts, err := c.GetNumOfDeployedProjects(tt.args.projectName) + assert.Nil(t, err) + assert.Equal(t, tt.args.numProjects, numBkts) + + // Adding using diff project name should create new bucket + err = c.AddProjectBuildData(tt.args.projectName+"_new", tt.args.metadata) + numBkts, err = c.GetNumOfDeployedProjects(tt.args.projectName) + assert.Nil(t, err) + assert.Equal(t, tt.args.numProjects+1, numBkts) + }) + } +} + func TestDataManager_destroy(t *testing.T) { dir := "./test_config" err := os.Mkdir(dir, os.ModePerm) diff --git a/daemon/inertiad/project/deployment.go b/daemon/inertiad/project/deployment.go index 6c7527e9..8edc625c 100644 --- a/daemon/inertiad/project/deployment.go +++ b/daemon/inertiad/project/deployment.go @@ -36,6 +36,8 @@ type Deployer interface { GetBranch() string CompareRemotes(string) error + UpdateContainerHistory(cli *docker.Client) error + GetDataManager() (*DeploymentDataManager, bool) Watch(*docker.Client) (<-chan string, <-chan error) @@ -70,6 +72,15 @@ type DeploymentConfig struct { PemFilePath string } +// DeploymentMetadata is used to store metadata relevant +// to the most recent deployment +type DeploymentMetadata struct { + Hash string + ContainerID string + ContainerStatus string + StartedAt string +} + // NewDeployment creates a new deployment func NewDeployment( projectDirectory string, @@ -314,6 +325,59 @@ func (d *Deployment) CompareRemotes(remoteURL string) error { return nil } +// UpdateContainerHistory will update container bucket with recent build's +// metadata +func (d *Deployment) UpdateContainerHistory(cli *docker.Client) error { + + // Get project hash + head, err := d.repo.Head() + if err != nil { + return fmt.Errorf("failed fetching repo head when updating container history: %s", err.Error()) + } + // Retrieve container for recently deployed project + ctx := context.Background() + var recentlyBuiltContainer types.Container + containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) + if err != nil { + return fmt.Errorf("failure fetching list of containers: %s", err.Error()) + } + for _, container := range containers { + if container.Names[0] == d.project { + recentlyBuiltContainer = container + } + } + + // Get container metadata + var containerID string + if len(recentlyBuiltContainer.ID) > 0 { + containerID = recentlyBuiltContainer.ID + } + containerJSON, err := cli.ContainerInspect(ctx, containerID) + if err != nil { + return fmt.Errorf("failure fetching container metadata: %s", err.Error()) + } + containerState := containerJSON.ContainerJSONBase.State // similar to running "docker inspect {container}" + var containerStatus, containerStartedAtTime string + if containerState != nil { + containerStatus = containerState.Status + containerStartedAtTime = containerState.StartedAt + } + + metadata := DeploymentMetadata{ + Hash: head.Hash().String(), + ContainerID: containerID, + ContainerStatus: containerStatus, + StartedAt: containerStartedAtTime} + + // Update db with newly built container metadata + err = d.dataManager.AddProjectBuildData(d.project, metadata) + if err != nil { + return fmt.Errorf("failure adding build metadata: %s", err.Error()) + } + + return nil +} + // GetDataManager returns the class managing deployment data func (d *Deployment) GetDataManager() (manager *DeploymentDataManager, found bool) { if d.dataManager == nil { diff --git a/daemon/inertiad/project/mocks/deployer.go b/daemon/inertiad/project/mocks/deployer.go index dbf23cd7..fa2db469 100644 --- a/daemon/inertiad/project/mocks/deployer.go +++ b/daemon/inertiad/project/mocks/deployer.go @@ -125,6 +125,17 @@ type FakeDeployer struct { setConfigArgsForCall []struct { arg1 project.DeploymentConfig } + UpdateContainerHistoryStub func(*client.Client) error + updateContainerHistoryMutex sync.RWMutex + updateContainerHistoryArgsForCall []struct { + arg1 *client.Client + } + updateContainerHistoryReturns struct { + result1 error + } + updateContainerHistoryReturnsOnCall map[int]struct { + result1 error + } WatchStub func(*client.Client) (<-chan string, <-chan error) watchMutex sync.RWMutex watchArgsForCall []struct { @@ -712,6 +723,66 @@ func (fake *FakeDeployer) SetConfigArgsForCall(i int) project.DeploymentConfig { return argsForCall.arg1 } +func (fake *FakeDeployer) UpdateContainerHistory(arg1 *client.Client) error { + fake.updateContainerHistoryMutex.Lock() + ret, specificReturn := fake.updateContainerHistoryReturnsOnCall[len(fake.updateContainerHistoryArgsForCall)] + fake.updateContainerHistoryArgsForCall = append(fake.updateContainerHistoryArgsForCall, struct { + arg1 *client.Client + }{arg1}) + fake.recordInvocation("UpdateContainerHistory", []interface{}{arg1}) + fake.updateContainerHistoryMutex.Unlock() + if fake.UpdateContainerHistoryStub != nil { + return fake.UpdateContainerHistoryStub(arg1) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.updateContainerHistoryReturns + return fakeReturns.result1 +} + +func (fake *FakeDeployer) UpdateContainerHistoryCallCount() int { + fake.updateContainerHistoryMutex.RLock() + defer fake.updateContainerHistoryMutex.RUnlock() + return len(fake.updateContainerHistoryArgsForCall) +} + +func (fake *FakeDeployer) UpdateContainerHistoryCalls(stub func(*client.Client) error) { + fake.updateContainerHistoryMutex.Lock() + defer fake.updateContainerHistoryMutex.Unlock() + fake.UpdateContainerHistoryStub = stub +} + +func (fake *FakeDeployer) UpdateContainerHistoryArgsForCall(i int) *client.Client { + fake.updateContainerHistoryMutex.RLock() + defer fake.updateContainerHistoryMutex.RUnlock() + argsForCall := fake.updateContainerHistoryArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeDeployer) UpdateContainerHistoryReturns(result1 error) { + fake.updateContainerHistoryMutex.Lock() + defer fake.updateContainerHistoryMutex.Unlock() + fake.UpdateContainerHistoryStub = nil + fake.updateContainerHistoryReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeDeployer) UpdateContainerHistoryReturnsOnCall(i int, result1 error) { + fake.updateContainerHistoryMutex.Lock() + defer fake.updateContainerHistoryMutex.Unlock() + fake.UpdateContainerHistoryStub = nil + if fake.updateContainerHistoryReturnsOnCall == nil { + fake.updateContainerHistoryReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateContainerHistoryReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeDeployer) Watch(arg1 *client.Client) (<-chan string, <-chan error) { fake.watchMutex.Lock() ret, specificReturn := fake.watchReturnsOnCall[len(fake.watchArgsForCall)] @@ -798,6 +869,8 @@ func (fake *FakeDeployer) Invocations() map[string][][]interface{} { defer fake.pruneMutex.RUnlock() fake.setConfigMutex.RLock() defer fake.setConfigMutex.RUnlock() + fake.updateContainerHistoryMutex.RLock() + defer fake.updateContainerHistoryMutex.RUnlock() fake.watchMutex.RLock() defer fake.watchMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/make b/make new file mode 100644 index 00000000..e69de29b diff --git a/test/keys/id_rsa b/test/keys/id_rsa old mode 100755 new mode 100644