aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorChristine Dodrill <me@christine.website>2017-03-29 11:15:52 -0700
committerChristine Dodrill <me@christine.website>2017-03-29 11:15:52 -0700
commit5bbf3a6186aebacb09605679aabdeaf8a0f61c4f (patch)
tree73bf2766f2b45ead6661186c309249038eae76da /tools
parente7824cb2609cc32d4229c9a7bc0de7b7153d36ef (diff)
downloadx-5bbf3a6186aebacb09605679aabdeaf8a0f61c4f.tar.xz
x-5bbf3a6186aebacb09605679aabdeaf8a0f61c4f.zip
move svc and yk
Diffstat (limited to 'tools')
-rw-r--r--tools/svc/GOALS.md79
-rw-r--r--tools/svc/cmd/svc/main.go313
-rw-r--r--tools/svc/cmd/svcd/dockerswarm-svcd/.gitignore4
-rw-r--r--tools/svc/cmd/svcd/dockerswarm-svcd/main.go497
-rw-r--r--tools/svc/credentials/jwt/jwt.go24
-rw-r--r--tools/svc/methods/docker.lua43
-rwxr-xr-xtools/svc/proto/regen.sh19
-rw-r--r--tools/svc/proto/svc.pb.go607
-rw-r--r--tools/svc/proto/svc.pb.gw.go340
-rw-r--r--tools/svc/proto/svc.proto95
-rw-r--r--tools/svc/proto/svc.swagger.json330
-rw-r--r--tools/svc/sample/health.lua16
-rw-r--r--tools/svc/sample/manifest.yaml28
-rw-r--r--tools/yk/main.go48
14 files changed, 2443 insertions, 0 deletions
diff --git a/tools/svc/GOALS.md b/tools/svc/GOALS.md
new file mode 100644
index 0000000..3abe8df
--- /dev/null
+++ b/tools/svc/GOALS.md
@@ -0,0 +1,79 @@
+# svc
+## Goals
+
+- Standardize service deployments to have _one_ syntax and _one_ function for the following:
+ 1. Deployment
+ 2. Checking the status of a deployed service
+ 3. Killing off an old instance of the service
+
+- Create a command line tool that deploys a service to a given provider
+ given configuration in a simple yaml manifest (see example [here](https://github.com/Xe/tools/tree/master/svc/sample))
+
+- Persist a mapping of service names -> identifier for keeping track of past deployments
+
+## Subcommands
+
+| cmd | what it does |
+|:--- |:------------ |
+| `spawn` | Launches a new instance of the given service name on the given backend |
+| `ps` | Inquires the status of all known deployed services and displays them in a clever little grid |
+| `create` | Creates a directory hierarchy at $SVCROOT for a new service by name |
+| `remove` | Stops a service and undeploys it from a given backend |
+| `cycle` | Pulls the latest image and restarts the service with the new image |
+| `inspect` | Inspects a single service, outputting its state in json |
+
+### `spawn`
+
+Launches a new instance of the given service name on the given backend
+
+Usage: `svc spawn [options] <servicename> <backend>`
+
+Options:
+
+| option | type | effect |
+|:------ |:---- |:------ |
+| `-kahled` | bool | Creates another instance of this service if one exists on any backend, fails if service is exclusive and already spawned |
+
+### `ps`
+
+Inquires the status of all known deployed services and displays them in a clever little grid
+
+Usage `svc ps [options] [servicename]`
+
+Options:
+
+| option | type | effect |
+|:------ |:---- |:------ |
+| `-backend` | string | If set, only show results for services running on the given backend |
+| `-match` | string | If set, regex-match on service details |
+| `-format` | string | Pretty-print container status using a Go template |
+
+### `create`
+
+Creates a directory hierarchy at $SVCROOT for a new service by name
+
+Usage: `svc create <servicename>`
+
+### `remove`
+
+Stops a service and removes it from a given backend
+
+Usage: `svc remove <servicename>`
+
+### `cycle`
+
+Pulls the latest image and restarts the service with the new image
+
+This command ***NEVER*** stops the old container until the new container is running and passes
+healthchecks.
+
+Usage: `svc cycle <servicename>`
+
+### `inspect`
+
+Inspects a single service from a single backend, outputting its state in json
+
+By default this will output a list of the inspect state of all matching instances of a service
+running on a particular backend.
+
+Usage: `svc inspect <servicename> <backend>`
diff --git a/tools/svc/cmd/svc/main.go b/tools/svc/cmd/svc/main.go
new file mode 100644
index 0000000..3d72e1e
--- /dev/null
+++ b/tools/svc/cmd/svc/main.go
@@ -0,0 +1,313 @@
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/Bowery/prompt"
+ jwtcreds "github.com/Xe/tools/svc/credentials/jwt"
+ svc "github.com/Xe/tools/svc/proto"
+ "github.com/Xe/uuid"
+ jwt "github.com/dgrijalva/jwt-go"
+ "github.com/joho/godotenv"
+ "github.com/olekukonko/tablewriter"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
+ kingpin "gopkg.in/alecthomas/kingpin.v1"
+)
+
+var (
+ app = kingpin.New("svc", "A simple service manager")
+ debug = app.Flag("debug", "print debugging logs?").Bool()
+ host = app.Flag("host", "host to do these changes to").String()
+ dataDir = app.Flag("data-dir", "place for svc to store data").Default(filepath.Join(os.Getenv("HOME"), ".local/within/svc")).String()
+
+ create = app.Command("create", "Create a new application")
+ createName = create.Flag("name", "name of the application").Required().String()
+ createEnvFile = create.Flag("env-file", "file with key->value envvars").ExistingFile()
+ createEnvironment = create.Flag("env", "environment variables for the program").StringMap()
+ createLabels = create.Flag("label", "additional labels to attach to the service").StringMap()
+ //createAuthorizedUsers = create.Flag("authorized-user", "additional user to allow modification access to").Strings()
+ //createExclusive = create.Flag("exclusive", "can this only ever have one copy running at once?").Bool()
+ //createInstances = create.Flag("instances", "number of instances of the backend service").Default("1").Int()
+ createDockerImage = create.Arg("docker image", "docker image to execute for this service").Required().String()
+
+ createToken = app.Command("create-token", "Creates the initial server control token")
+ createTokenJwtSecret = createToken.Flag("jwt-secret", "jwt secret used on the server").Required().String()
+ createTokenUsername = createToken.Arg("username", "username to create token for").Required().String()
+
+ deleteCmd = app.Command("delete", "Deletes an application by name")
+ deleteName = deleteCmd.Arg("name", "name of the service").Required().String()
+
+ hostCmd = app.Command("host", "Host management")
+ hostAdd = hostCmd.Command("add", "Add a host to the state file")
+ hostAddTor = hostAdd.Flag("tor", "connect to this over tor?").Bool()
+ hostAddCaCert = hostAdd.Flag("ca-cert", "ca certificate of the server").Default("ca.pem").File()
+ hostAddCert = hostAdd.Flag("cert", "client certificate").Default("cert.pem").File()
+ hostAddKey = hostAdd.Flag("key", "client ssl key").Default("key.pem").File()
+ hostAddName = hostAdd.Arg("name", "name of host to add").Required().String()
+ hostAddAddr = hostAdd.Arg("addr", "address of taget server (host:port)").Required().String()
+
+ hostRemove = hostCmd.Command("remove", "Remove a host from the state file")
+ hostRemoveName = hostRemove.Arg("name", "name of host to remove").Required().String()
+
+ inspect = app.Command("inspect", "Inspect an application")
+ inspectName = inspect.Arg("name", "name of the service").String()
+
+ list = app.Command("list", "List apps running with this backend")
+ listLabelKey = list.Flag("labelKey", "label key to match for").String()
+ listLabelValue = list.Flag("labelValue", "label value to match for (with labelKey)").String()
+
+ update = app.Command("update", "Update an application")
+ updateImage = update.Flag("image", "new docker image to use for this service").String()
+ updateEnvAdd = update.Flag("env-add", "new environment variables to set").StringMap()
+ updateEnvRm = update.Flag("env-rm", "environment variables to remove").Strings()
+ updateGrantUsers = update.Flag("grant-user", "grant a user permission to this service").Strings()
+ updateRevokeUsers = update.Flag("revoke-user", "revoke a user's permission to this service").Strings()
+ updateName = update.Flag("name", "name of the service to update").Required().String()
+)
+
+func main() {
+ cmdline := kingpin.MustParse(app.Parse(os.Args[1:]))
+
+ state, err := readState()
+ if err != nil {
+ if os.IsNotExist(err) {
+ log.Println("Host file does not exist, please add a host with `svc host add`.")
+ }
+
+ log.Fatal(err)
+ }
+ writeState(state)
+
+ switch cmdline {
+ case "host add":
+ token, err := prompt.Basic("token: ", true)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ caCertData, err := ioutil.ReadAll(*hostAddCaCert)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ clientCertData, err := ioutil.ReadAll(*hostAddCert)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ clientKeyData, err := ioutil.ReadAll(*hostAddKey)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ h := &Host{
+ Name: *hostAddName,
+ Addr: *hostAddAddr,
+ Token: token,
+ Tor: *hostAddTor,
+ CaCert: caCertData,
+ Cert: clientCertData,
+ Key: clientKeyData,
+ }
+
+ state.Hosts[h.Name] = h
+ writeState(state)
+
+ log.Println("Host added to hosts file.")
+ return
+ case "host remove":
+ _, exists := state.Hosts[*hostRemoveName]
+ if !exists {
+ log.Fatalf("no such host %q", *hostRemoveName)
+ }
+
+ delete(state.Hosts, *hostRemoveName)
+ writeState(state)
+
+ log.Printf("Host %q removed from hosts file", *hostRemoveName)
+ return
+ case "create-token":
+ now := time.Now()
+
+ hostname, _ := os.Hostname()
+ tid := uuid.New()
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS512, &jwt.StandardClaims{
+ IssuedAt: now.Unix(),
+ Issuer: hostname,
+ Subject: *createTokenUsername,
+ Id: tid,
+ })
+
+ tokenString, err := token.SignedString([]byte(*createTokenJwtSecret))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Println(tokenString)
+
+ os.Exit(0)
+ }
+
+ if *host == "" {
+ log.Fatal("--host must be supplied")
+ }
+
+ hostInfo, ok := state.Hosts[*host]
+ if !ok {
+ log.Fatalf("Requested host %q that doesn't exist in state", *host)
+ }
+
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(hostInfo.CaCert)
+
+ connCreds := credentials.NewTLS(&tls.Config{
+ RootCAs: caCertPool,
+ InsecureSkipVerify: true,
+ })
+
+ creds := jwtcreds.NewFromToken(hostInfo.Token)
+ conn, err := grpc.Dial(hostInfo.Addr,
+ grpc.WithTransportCredentials(connCreds),
+ grpc.WithPerRPCCredentials(creds))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ c := svc.NewAppsClient(conn)
+
+ // RPC commands
+ switch cmdline {
+ case list.FullCommand():
+ apps, err := c.List(context.Background(), &svc.AppsListParams{})
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ table := tablewriter.NewWriter(os.Stdout)
+
+ table.SetHeader([]string{"ID", "Name", "Image", "Users"})
+
+ for _, app := range apps.Apps {
+ table.Append([]string{app.Id, app.Name, app.DockerImage, fmt.Sprintf("%v", app.AuthorizedUsers)})
+ }
+ table.Render()
+
+ case create.FullCommand():
+ env := map[string]string{}
+
+ for key, val := range *createEnvironment {
+ env[key] = val
+ }
+
+ if *createEnvFile != "" {
+ emap, err := godotenv.Read(*createEnvFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for key, val := range emap {
+ env[key] = val
+ }
+ }
+
+ m := &svc.Manifest{
+ DockerImage: *createDockerImage,
+ Environment: env,
+ Labels: *createLabels,
+ Name: *createName,
+ }
+
+ app, err := c.Create(context.Background(), m)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Printf("%s created", app.Name)
+ return
+ case update.FullCommand():
+ _, err := c.Update(context.Background(), &svc.AppUpdate{
+ Name: *updateName,
+ NewImage: *updateImage,
+ EnvAdd: *updateEnvAdd,
+ EnvRm: *updateEnvRm,
+ GrantUsers: *updateGrantUsers,
+ RevokeUsers: *updateRevokeUsers,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Println("success")
+ return
+ case inspect.FullCommand():
+ app, err := c.Inspect(context.Background(), &svc.AppInspect{
+ Name: *inspectName,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ e := json.NewEncoder(os.Stdout)
+ e.SetIndent("", " ")
+ e.Encode(app)
+ return
+ case deleteCmd.FullCommand():
+ ok, err := c.Delete(context.Background(), &svc.AppDelete{Name: *deleteName})
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Println(ok.Message)
+ }
+}
+
+type state struct {
+ Hosts map[string]*Host
+}
+
+type Host struct {
+ Name string
+ Addr string
+ Token string
+ Tor bool
+ CaCert []byte
+ Cert []byte
+ Key []byte
+}
+
+func readState() (*state, error) {
+ s := &state{}
+
+ fname := filepath.Join(*dataDir, "state.json")
+ fin, err := os.Open(fname)
+ if err != nil {
+ return nil, err
+ }
+ defer fin.Close()
+
+ err = json.NewDecoder(fin).Decode(s)
+
+ return s, err
+}
+
+func writeState(s *state) error {
+ fname := filepath.Join(*dataDir, "state.json")
+ fout, err := os.Create(fname)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ return json.NewEncoder(fout).Encode(s)
+}
diff --git a/tools/svc/cmd/svcd/dockerswarm-svcd/.gitignore b/tools/svc/cmd/svcd/dockerswarm-svcd/.gitignore
new file mode 100644
index 0000000..5b0c5e0
--- /dev/null
+++ b/tools/svc/cmd/svcd/dockerswarm-svcd/.gitignore
@@ -0,0 +1,4 @@
+state.json
+dockerswarm-svcd
+*.pem
+.env
diff --git a/tools/svc/cmd/svcd/dockerswarm-svcd/main.go b/tools/svc/cmd/svcd/dockerswarm-svcd/main.go
new file mode 100644
index 0000000..bbeef6d
--- /dev/null
+++ b/tools/svc/cmd/svcd/dockerswarm-svcd/main.go
@@ -0,0 +1,497 @@
+package main
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "strings"
+ "sync"
+
+ svc "github.com/Xe/tools/svc/proto"
+ jwt "github.com/dgrijalva/jwt-go"
+ "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/swarm"
+ "github.com/docker/docker/client"
+ "github.com/facebookgo/flagenv"
+ "github.com/grpc-ecosystem/grpc-gateway/runtime"
+ _ "github.com/joho/godotenv/autoload"
+ "golang.org/x/net/context"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials"
+ "google.golang.org/grpc/metadata"
+)
+
+var (
+ listenAddress = flag.String("listen", "127.0.0.1:23142", "tcp host:port to listen on")
+ sslCert = flag.String("tls-cert", "", "tls certificate to read from")
+ sslKey = flag.String("tls-key", "", "tls private key")
+ caCert = flag.String("ca-cert", "", "ca public cert")
+ jwtSecret = flag.String("jwt-secret", "hunter2", "secret used to sign jwt's")
+ httpAddress = flag.String("http-listen", "127.0.0.1:9090", "tcp host:port to listen the web server on")
+)
+
+const admin = "xena"
+
+type server struct {
+ docker *client.Client
+
+ sync.Mutex
+ state map[string][]string
+}
+
+func (s *server) LoadState(fname string) error {
+ s.Lock()
+ defer s.Unlock()
+
+ fin, err := os.Open(fname)
+ if err != nil {
+ return err
+ }
+ defer fin.Close()
+
+ return json.NewDecoder(fin).Decode(&s.state)
+}
+
+func (s *server) SaveState(fname string) error {
+ s.Lock()
+ defer s.Unlock()
+
+ fout, err := os.Create(fname)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ return json.NewEncoder(fout).Encode(&s.state)
+}
+
+func (s *server) List(ctx context.Context, params *svc.AppsListParams) (*svc.AppsList, error) {
+ user, err := s.checkAuth(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ svcs, err := s.docker.ServiceList(ctx, types.ServiceListOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ result := &svc.AppsList{}
+
+ for _, ssvc := range svcs {
+ env := func(kv []string) map[string]string {
+ result := map[string]string{}
+
+ for _, pair := range kv {
+ split := strings.SplitN(pair, "=", 2)
+ result[split[0]] = split[1]
+ }
+
+ return result
+ }(ssvc.Spec.TaskTemplate.ContainerSpec.Env)
+
+ au := s.state[ssvc.Spec.Name]
+ if au == nil {
+ s.state[ssvc.Spec.Name] = []string{admin}
+ s.SaveState("state.json")
+ }
+
+ allowed := false
+
+ if user == admin {
+ allowed = true
+ }
+
+ for _, allowedUser := range au {
+ if user == allowedUser {
+ allowed = true
+ }
+ }
+
+ if !allowed {
+ continue
+ }
+
+ result.Apps = append(result.Apps, &svc.App{
+ Id: ssvc.ID,
+ Name: ssvc.Spec.Name,
+ DockerImage: ssvc.Spec.TaskTemplate.ContainerSpec.Image,
+ Environment: env,
+ Labels: ssvc.Spec.Labels,
+ AuthorizedUsers: au,
+ Instances: int32(*ssvc.Spec.Mode.Replicated.Replicas),
+ Status: "",
+ })
+ }
+
+ return result, nil
+}
+
+func (s *server) Create(ctx context.Context, manifest *svc.Manifest) (*svc.App, error) {
+ user, err := s.checkAuth(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ if user != admin {
+ return nil, grpc.Errorf(codes.PermissionDenied, "create: permission denied for user %s", user)
+ }
+
+ env := []string{}
+
+ for key, val := range manifest.Environment {
+ env = append(env, fmt.Sprintf("%s=%s", key, val))
+ }
+
+ spec := swarm.ServiceSpec{
+ Annotations: swarm.Annotations{
+ Name: manifest.Name,
+ Labels: manifest.Labels,
+ },
+
+ TaskTemplate: swarm.TaskSpec{
+ ContainerSpec: swarm.ContainerSpec{
+ Image: manifest.DockerImage,
+ Env: env,
+ },
+ },
+
+ Mode: swarm.ServiceMode{
+ Replicated: &swarm.ReplicatedService{},
+ },
+ }
+
+ resp, err := s.docker.ServiceCreate(ctx, spec, types.ServiceCreateOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ ssvc, _, err := s.docker.ServiceInspectWithRaw(ctx, resp.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ app := &svc.App{
+ Id: ssvc.ID,
+ Name: ssvc.Spec.Name,
+ DockerImage: ssvc.Spec.TaskTemplate.ContainerSpec.Image,
+ Environment: manifest.Environment,
+ Labels: ssvc.Spec.Labels,
+ AuthorizedUsers: []string{user},
+ }
+
+ return app, nil
+}
+
+func (s *server) Update(ctx context.Context, params *svc.AppUpdate) (*svc.App, error) {
+ user, err := s.checkAuth(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ au := s.state[params.Name]
+ if au == nil {
+ s.state[params.Name] = []string{admin}
+ au = s.state[params.Name]
+ s.SaveState("state.json")
+ }
+
+ found := false
+ if user == admin {
+ found = true
+ }
+
+ for _, uu := range au {
+ if user == uu {
+ found = true
+ }
+ }
+
+ if !found {
+ return nil, grpc.Errorf(codes.PermissionDenied, "You do not have permission for this app")
+ }
+
+ found = false
+ var svcToUpdate swarm.Service
+
+ svcs, err := s.docker.ServiceList(ctx, types.ServiceListOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ for _, dsvc := range svcs {
+ if dsvc.Spec.Name == params.Name {
+ found = true
+ svcToUpdate = dsvc
+ }
+ }
+
+ if !found {
+ return nil, errors.New("service not found")
+ }
+
+ if params.NewImage != "" {
+ svcToUpdate.Spec.TaskTemplate.ContainerSpec.Image = params.NewImage
+ }
+
+ env := svcToUpdate.Spec.TaskTemplate.ContainerSpec.Env
+
+ for key, val := range params.EnvAdd {
+ env = append(env, fmt.Sprintf("%s=%s", key, val))
+ }
+
+ for _, varName := range params.EnvRm {
+ for i, envVar := range env {
+ if strings.HasPrefix(envVar, varName+"=") {
+ env[i] = env[len(env)-1]
+ env[len(env)-1] = ""
+ env = env[:len(env)-1]
+ }
+ }
+ }
+
+ if len(params.GrantUsers) != 0 {
+ s.Lock()
+ for _, u := range params.GrantUsers {
+ s.state[params.Name] = append(s.state[params.Name], u)
+ }
+ s.Unlock()
+ }
+
+ if len(params.RevokeUsers) != 0 {
+ s.Lock()
+ for _, u := range params.RevokeUsers {
+ for i, uu := range au {
+ if u == uu {
+ s.state[params.Name][i] = s.state[params.Name][len(s.state[params.Name])-1]
+ s.state[params.Name][len(s.state[params.Name])-1] = ""
+ s.state[params.Name] = s.state[params.Name][:len(s.state[params.Name])-1]
+ }
+ }
+ }
+ s.Unlock()
+ }
+
+ s.SaveState("state.json")
+
+ s.docker.ServiceUpdate(ctx, svcToUpdate.ID, svcToUpdate.Version, svcToUpdate.Spec, types.ServiceUpdateOptions{})
+
+ return s.Inspect(ctx, &svc.AppInspect{Name: params.Name})
+}
+
+func (s *server) Inspect(ctx context.Context, params *svc.AppInspect) (*svc.App, error) {
+ user, err := s.checkAuth(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ au := s.state[params.Name]
+ if au == nil {
+ s.state[params.Name] = []string{admin}
+ au = s.state[params.Name]
+ s.SaveState("state.json")
+ }
+
+ found := false
+ if user == admin {
+ found = true
+ }
+
+ for _, uu := range au {
+ if user == uu {
+ found = true
+ }
+ }
+
+ if !found {
+ return nil, grpc.Errorf(codes.PermissionDenied, "You do not have permission for this app")
+ }
+
+ svcs, err := s.docker.ServiceList(ctx, types.ServiceListOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ found = false
+ svcID := ""
+
+ for _, dsvc := range svcs {
+ if dsvc.Spec.Name == params.Name {
+ found = true
+ svcID = dsvc.ID
+ }
+ }
+
+ if !found {
+ return nil, errors.New("service not found")
+ }
+
+ dsvc, _, err := s.docker.ServiceInspectWithRaw(ctx, svcID)
+ if err != nil {
+ return nil, err
+ }
+
+ env := func(kv []string) map[string]string {
+ result := map[string]string{}
+
+ for _, pair := range kv {
+ split := strings.SplitN(pair, "=", 2)
+ result[split[0]] = split[1]
+ }
+
+ return result
+ }(dsvc.Spec.TaskTemplate.ContainerSpec.Env)
+
+ a := &svc.App{
+ Id: dsvc.ID,
+ Name: dsvc.Spec.Name,
+ DockerImage: dsvc.Spec.TaskTemplate.ContainerSpec.Image,
+ Environment: env,
+ Labels: dsvc.Spec.Labels,
+ AuthorizedUsers: au,
+ }
+
+ return a, nil
+}
+
+func (s *server) Delete(ctx context.Context, params *svc.AppDelete) (*svc.Ok, error) {
+ user, err := s.checkAuth(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ if user != admin {
+ return nil, grpc.Errorf(codes.PermissionDenied, "must be an admin to delete things")
+ }
+
+ svcs, err := s.docker.ServiceList(ctx, types.ServiceListOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ found := false
+
+ for _, dsvc := range svcs {
+ if dsvc.Spec.Name == params.Name {
+ found = true
+
+ err = s.docker.ServiceRemove(ctx, dsvc.ID)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ if !found {
+ return nil, errors.New("service not found")
+ }
+
+ return &svc.Ok{Message: "app " + params.Name + " deleted"}, nil
+}
+
+func main() {
+ flag.Parse()
+ flagenv.Parse()
+
+ var creds credentials.TransportCredentials
+ var gs *grpc.Server
+
+ if *sslCert != "" && *caCert != "" && *sslKey != "" {
+ cert, err := tls.LoadX509KeyPair(*sslCert, *sslKey)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ rawCaCert, err := ioutil.ReadFile(*caCert)
+ if err != nil {
+ log.Fatal(err)
+ }
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(rawCaCert)
+
+ creds = credentials.NewTLS(&tls.Config{
+ Certificates: []tls.Certificate{cert},
+ ClientCAs: caCertPool,
+ ClientAuth: tls.VerifyClientCertIfGiven,
+ })
+
+ gs = grpc.NewServer(grpc.Creds(creds))
+ } else {
+ gs = grpc.NewServer()
+ }
+
+ defaultHeaders := map[string]string{"User-Agent": "dockerswarm-svcd"}
+ cli, err := client.NewClient(client.DefaultDockerHost, client.DefaultVersion, nil, defaultHeaders)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ s := &server{
+ docker: cli,
+ state: map[string][]string{},
+ }
+
+ err = s.LoadState("state.json")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ svc.RegisterAppsServer(gs, s)
+
+ l, err := net.Listen("tcp", *listenAddress)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ mux := runtime.NewServeMux()
+ opts := []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
+ InsecureSkipVerify: true,
+ }))}
+ err = svc.RegisterAppsHandlerFromEndpoint(context.Background(), mux, *listenAddress, opts)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ go http.ListenAndServe(*httpAddress, mux)
+
+ err = gs.Serve(l)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func (s *server) checkAuth(ctx context.Context) (string, error) {
+ var err error
+
+ md, ok := metadata.FromContext(ctx)
+ if !ok {
+ return "", grpc.Errorf(codes.Unauthenticated, "valid token required.")
+ }
+
+ jwtToken, ok := md["authorization"]
+ if !ok {
+ return "", grpc.Errorf(codes.Unauthenticated, "valid token required.")
+ }
+
+ clms := &jwt.StandardClaims{}
+
+ p := &jwt.Parser{}
+ _, err = p.ParseWithClaims(jwtToken[0], clms, jwt.Keyfunc(func(t *jwt.Token) (interface{}, error) {
+ return []byte(*jwtSecret), nil
+ }))
+ if err != nil {
+ log.Printf("rpc error: %v", err)
+ return "", grpc.Errorf(codes.Unauthenticated, "valid token requried.")
+ }
+
+ return clms.Subject, nil
+}
diff --git a/tools/svc/credentials/jwt/jwt.go b/tools/svc/credentials/jwt/jwt.go
new file mode 100644
index 0000000..978c880
--- /dev/null
+++ b/tools/svc/credentials/jwt/jwt.go
@@ -0,0 +1,24 @@
+package jwt
+
+import (
+ "golang.org/x/net/context"
+ "google.golang.org/grpc/credentials"
+)
+
+type jwt struct {
+ token string
+}
+
+func NewFromToken(token string) credentials.PerRPCCredentials {
+ return jwt{token: token}
+}
+
+func (j jwt) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
+ return map[string]string{
+ "authorization": j.token,
+ }, nil
+}
+
+func (j jwt) RequireTransportSecurity() bool {
+ return false
+}
diff --git a/tools/svc/methods/docker.lua b/tools/svc/methods/docker.lua
new file mode 100644
index 0000000..766718f
--- /dev/null
+++ b/tools/svc/methods/docker.lua
@@ -0,0 +1,43 @@
+local json = require "json"
+local sh = require "sh"
+
+if os.getenv("DIE_ON_ERROR") == "yes" then
+ sh { abort = true }
+end
+
+-- given the name, docker image name and environment
+-- variables for this service, deploy it via Docker
+-- running locally.
+function deploy(name, imagename, vars, settings)
+ args = { "run", "-d", "--name", name, "--label", "xe.svc.name="..name}
+
+ for k,v in pairs(vars) do
+ table.insert(args, "--env")
+ table.insert(args, k .. "=" .. v)
+ end
+
+ table.insert(args, imagename)
+
+ local cmd = sh.docker(unpack(args))
+ cmd:ok()
+
+ local ctrid = cmd:lines()()
+ return ctrid
+end
+
+-- given a container name, return a table of information
+-- about it
+function inspect(name)
+ local obj = sh.docker("inspect", ctrid):combinedOutput()
+ local tbl, err = json.decode(obj)
+ if err ~= nil then
+ error(err, obj)
+ end
+
+ return tbl
+end
+
+-- kill a container by a given name
+function kill(name)
+ sh.docker("rm", "-f", name):ok()
+end
diff --git a/tools/svc/proto/regen.sh b/tools/svc/proto/regen.sh
new file mode 100755
index 0000000..cb20e86
--- /dev/null
+++ b/tools/svc/proto/regen.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+protoc -I/usr/local/include -I. \
+ -I$GOPATH/src \
+ -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
+ --go_out=Mgoogle/api/annotations.proto=github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api,plugins=grpc:. \
+ svc.proto
+
+protoc -I/usr/local/include -I. \
+ -I$GOPATH/src \
+ -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
+ --grpc-gateway_out=logtostderr=true:. \
+ svc.proto
+
+protoc -I/usr/local/include -I. \
+ -I$GOPATH/src \
+ -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
+ --swagger_out=logtostderr=true:. \
+ svc.proto
diff --git a/tools/svc/proto/svc.pb.go b/tools/svc/proto/svc.pb.go
new file mode 100644
index 0000000..3d83648
--- /dev/null
+++ b/tools/svc/proto/svc.pb.go
@@ -0,0 +1,607 @@
+// Code generated by protoc-gen-go.
+// source: svc.proto
+// DO NOT EDIT!
+
+/*
+Package svc is a generated protocol buffer package.
+
+It is generated from these files:
+ svc.proto
+
+It has these top-level messages:
+ AppsListParams
+ AppsList
+ Manifest
+ App
+ AppUpdate
+ AppInspect
+ AppDelete
+ Ok
+*/
+package svc
+
+import proto "github.com/golang/protobuf/proto"
+import fmt "fmt"
+import math "math"
+import _ "github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api"
+
+import (
+ context "golang.org/x/net/context"
+ grpc "google.golang.org/grpc"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
+
+type AppsListParams struct {
+ LabelKey string `protobuf:"bytes,1,opt,name=labelKey" json:"labelKey,omitempty"`
+ LabelVal string `protobuf:"bytes,2,opt,name=labelVal" json:"labelVal,omitempty"`
+ // will be matched to app names
+ Name string `protobuf:"bytes,3,opt,name=name" json:"name,omitempty"`
+}
+
+func (m *AppsListParams) Reset() { *m = AppsListParams{} }
+func (m *AppsListParams) String() string { return proto.CompactTextString(m) }
+func (*AppsListParams) ProtoMessage() {}
+func (*AppsListParams) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
+
+func (m *AppsListParams) GetLabelKey() string {
+ if m != nil {
+ return m.LabelKey
+ }
+ return ""
+}
+
+func (m *AppsListParams) GetLabelVal() string {
+ if m != nil {
+ return m.LabelVal
+ }
+ return ""
+}
+
+func (m *AppsListParams) GetName() string {
+ if m != nil {
+ return m.Name
+ }
+ return ""
+}
+
+type AppsList struct {
+ Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
+ Apps []*App `protobuf:"bytes,2,rep,name=apps" json:"apps,omitempty"`
+}
+
+func (m *AppsList) Reset() { *m = AppsList{} }
+func (m *AppsList) String() string { return proto.CompactTextString(m) }
+func (*AppsList) ProtoMess