Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Package 12factor and new Scheduler interface #696

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions 12factor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 12factor

12factor is a Go library for describing and running [12factor](http://12factor.net/) applications.

## Packages

* **[scheduler](./scheduler)**: Provides various implementations of the Scheduler interface for running 12factor apps. Implementations include Docker, ECS, Kubernetes and Nomad.
* **[procfile](./procfile)**: Provides methods for parsing the Procfile manifest format.

## Terminology

### App

An App describes a common environment and root filesystem, which is generally specified as a Docker container.

### Process

A Process represents a named command that can be scaled horizontally.

### Manifest

A manifest is the composition of an App and its Processes.
52 changes: 52 additions & 0 deletions 12factor/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package twelvefactor_test

import (
"log"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/remind101/empire/12factor"
"github.com/remind101/empire/12factor/scheduler/ecs"
)

// Simple example that shows how to run an application with the ECS scheduling
// backend.
func Example() {
// This is hard coded, but the end goal would be to provide methods for
// parsing Procfile and docker-compose.yml files into this Manifest
// type.
m := twelvefactor.Manifest{
App: twelvefactor.App{
ID: "acme-inc",
Name: "acme-inc",
Image: "remind101/acme-inc:master",
},
Processes: []twelvefactor.Process{
{
Name: "web",
Command: []string{"acme-inc server"},
},
},
}

// Use ECS as a scheduling backend. Our application will be run as ECS
// services.
scheduler := ecs.NewScheduler(session.New())

// Bring up the application. Creates ECS services as necessary.
err := scheduler.Up(m)
if err != nil {
log.Fatal(err)
}

// Scale up our web process.
err = scheduler.ScaleProcess(m.ID, "web", 2)
if err != nil {
log.Fatal(err)
}

// Remove the ECS resources.
err = scheduler.Remove(m.ID)
if err != nil {
log.Fatal(err)
}
}
60 changes: 60 additions & 0 deletions 12factor/scheduler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package twelvefactor

// Upper is an interface that wraps the basic Run method, providing a way to
// run a 12factor application.
//
// Implementors should ensure that any existing processes that don't exist in
// the newly submitted process list are removed. For example, if a "web" process
// was previously defined, then only a "worker" process was submitted, the
// existing "web" process should be removed.
type Upper interface {
Up(Manifest) error
}

// ProcessRunner is an optional interface for running attached and detached
// processes. This is useful for running attached processes like a rails
// console or detached processes like database migrations.
//
// Attached vs Detached is determined from the Stdout stream.
type ProcessRunner interface {
RunProcess(app string, process Process) error
}

// ProcessScaler is an interface that wraps the basic Scale method for scaling a
// process by name for an application.
type ProcessScaler interface {
ScaleProcess(app, process string, desired int) error
}

// Remover is an interface that wraps the basic Remove method for removing an
// app and all of it's processes.
type Remover interface {
Remove(app string) error
}

// Restarter is an interface that wraps the Restart method, which provides a
// method for restarting an App.
type Restarter interface {
Restart(app string) error
}

// ProcessRestarter is an interface that wraps the RestartProcess method, which
// provides a method for restarting a Process.
type ProcessRestarter interface {
RestartProcess(app string, process string) error
}

// Scheduler provides an interface for running twelve factor applications.
type Scheduler interface {
Upper
Remover
ProcessScaler
Restarter
ProcessRestarter

// Returns the tasks for the given application.
Tasks(app string) ([]Task, error)

// Stops an individual task.
StopTask(taskID string) error
}
Empty file.
37 changes: 37 additions & 0 deletions 12factor/scheduler/docker/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package docker

import (
"github.com/fsouza/go-dockerclient"
"github.com/remind101/empire/12factor"
)

// dockerClient represents the docker Client.
type dockerClient interface{}

// Scheduler is an implementation of the twelvefactor.Scheduler interface that
// talks to the Docker daemon API.
type Scheduler struct {
docker dockerClient
}

// NewScheduler returns a new Scheduler instance backed by the docker client.
func NewScheduler(c *docker.Client) *Scheduler {
return &Scheduler{
docker: c,
}
}

// NewSchedulerFromEnv returns a new Scheduler instance with a Docker client
// configured from the environment.
func NewSchedulerFromEnv() (*Scheduler, error) {
c, err := docker.NewClientFromEnv()
if err != nil {
return nil, err
}
return NewScheduler(c), nil
}

// Run runs the application with Docker.
func (s *Scheduler) Run(manifest twelvefactor.Manifest) error {
return nil
}
44 changes: 44 additions & 0 deletions 12factor/scheduler/docker/tests/docker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package docker_test

import (
"testing"

"github.com/remind101/empire/12factor"
"github.com/remind101/empire/12factor/scheduler/docker"
)

// manifest is our test application. This is a valid application that will be run
// with the docker daemon.
var manifest = twelvefactor.Manifest{
App: twelvefactor.App{
ID: "acme",
Name: "acme",
Image: "remind101/acme-inc",
Version: "v1",
Env: map[string]string{
"RAILS_ENV": "production",
},
},
Processes: []twelvefactor.Process{
{
Name: "web",
Command: []string{"acme-inc", "web"},
},
},
}

func TestScheduler_Run(t *testing.T) {
s := newScheduler(t)

if err := s.Run(manifest); err != nil {
t.Fatal(err)
}
}

func newScheduler(t testing.TB) *docker.Scheduler {
s, err := docker.NewSchedulerFromEnv()
if err != nil {
t.Fatalf("Could not build docker scheduler: %v", err)
}
return s
}
Empty file added 12factor/scheduler/ecs/.gitkeep
Empty file.
Empty file.
137 changes: 137 additions & 0 deletions 12factor/scheduler/ecs/cloudformation/cloudformation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Package cloudformation provides a StackBuilder for provisioning the AWS
// resources for an App using a CloudFormation stack.
package cloudformation

import (
"bytes"
"encoding/json"
"io"
"text/template"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/remind101/empire/12factor"
)

// BasicTemplate is a basic Template that creates a cloudformation stack.
var BasicTemplate = YAMLTemplate(template.Must(template.New("stack").Parse(`
Resources: {{range .Processes}}
{{.Name}}:
Type: AWS::ECS::Cluster{{end}}
`)))

const ecsServiceType = "AWS::ECS::Service"

type cloudformationClient interface {
CreateStack(input *cloudformation.CreateStackInput) (*cloudformation.CreateStackOutput, error)
DeleteStack(input *cloudformation.DeleteStackInput) (*cloudformation.DeleteStackOutput, error)
ListStackResourcesPages(*cloudformation.ListStackResourcesInput, func(*cloudformation.ListStackResourcesOutput, bool) bool) error
DescribeStackResource(input *cloudformation.DescribeStackResourceInput) (*cloudformation.DescribeStackResourceOutput, error)
WaitUntilStackCreateComplete(input *cloudformation.DescribeStacksInput) error
}

type serviceMetadata struct {
Name string `json:"name"`
}

// Template represents something that can generate a stack body.
type Template interface {
Execute(io.Writer, interface{}) error
}

// StackBuilder is an implementation of the ecs.StackBuilder interface that
// builds the stack using CloudFormation.
type StackBuilder struct {
// Template is a text/template that will be executed using the
// twelvefactor.Manifest as data. This template should return a valid
// CloudFormation JSON manifest.
Template Template

// stackName returns the name of the stack for the app.
stackName func(app string) string

cloudformation cloudformationClient
}

// NewStackBuilder returns a new StackBuilder instance.
func NewStackBuilder(config client.ConfigProvider) *StackBuilder {
return &StackBuilder{
cloudformation: cloudformation.New(config),
stackName: stackName,
}
}

// Build builds the CloudFormation stack for the App.
func (b *StackBuilder) Build(m twelvefactor.Manifest) error {
stack := b.stackName(m.ID)

buf := new(bytes.Buffer)
if err := b.Template.Execute(buf, m); err != nil {
return err
}

if _, err := b.cloudformation.CreateStack(&cloudformation.CreateStackInput{
StackName: aws.String(stack),
TemplateBody: aws.String(buf.String()),
}); err != nil {
return err
}

if err := b.cloudformation.WaitUntilStackCreateComplete(&cloudformation.DescribeStacksInput{
StackName: aws.String(stack),
}); err != nil {
return err
}

return nil
}

func (b *StackBuilder) Remove(app string) error {
return nil
}

// Services returns a mapping of process -> ecs service. It assumes the ECS
// service resources in the cloudformation template have metadata that includes
// a "Name" key that specifies the process name.
func (b *StackBuilder) Services(app string) (map[string]string, error) {
stack := b.stackName(app)

// Get a summary of all of the stacks resources.
var summaries []*cloudformation.StackResourceSummary
if err := b.cloudformation.ListStackResourcesPages(&cloudformation.ListStackResourcesInput{
StackName: aws.String(stack),
}, func(p *cloudformation.ListStackResourcesOutput, lastPage bool) bool {
summaries = append(summaries, p.StackResourceSummaries...)
return true
}); err != nil {
return nil, err
}

services := make(map[string]string)
for _, summary := range summaries {
if *summary.ResourceType == ecsServiceType {
resp, err := b.cloudformation.DescribeStackResource(&cloudformation.DescribeStackResourceInput{
StackName: aws.String(stack),
LogicalResourceId: summary.LogicalResourceId,
})
if err != nil {
return services, err
}

var meta serviceMetadata
if err := json.Unmarshal([]byte(*resp.StackResourceDetail.Metadata), &meta); err != nil {
return services, err
}

services[meta.Name] = *resp.StackResourceDetail.PhysicalResourceId
}
}

return services, nil
}

// stackName returns a stack name for the app id.
func stackName(app string) string {
return app
}
Loading