diff options
| author | Christine Dodrill <me@christine.website> | 2017-03-29 11:15:52 -0700 |
|---|---|---|
| committer | Christine Dodrill <me@christine.website> | 2017-03-29 11:15:52 -0700 |
| commit | 5bbf3a6186aebacb09605679aabdeaf8a0f61c4f (patch) | |
| tree | 73bf2766f2b45ead6661186c309249038eae76da /tools | |
| parent | e7824cb2609cc32d4229c9a7bc0de7b7153d36ef (diff) | |
| download | x-5bbf3a6186aebacb09605679aabdeaf8a0f61c4f.tar.xz x-5bbf3a6186aebacb09605679aabdeaf8a0f61c4f.zip | |
move svc and yk
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/svc/GOALS.md | 79 | ||||
| -rw-r--r-- | tools/svc/cmd/svc/main.go | 313 | ||||
| -rw-r--r-- | tools/svc/cmd/svcd/dockerswarm-svcd/.gitignore | 4 | ||||
| -rw-r--r-- | tools/svc/cmd/svcd/dockerswarm-svcd/main.go | 497 | ||||
| -rw-r--r-- | tools/svc/credentials/jwt/jwt.go | 24 | ||||
| -rw-r--r-- | tools/svc/methods/docker.lua | 43 | ||||
| -rwxr-xr-x | tools/svc/proto/regen.sh | 19 | ||||
| -rw-r--r-- | tools/svc/proto/svc.pb.go | 607 | ||||
| -rw-r--r-- | tools/svc/proto/svc.pb.gw.go | 340 | ||||
| -rw-r--r-- | tools/svc/proto/svc.proto | 95 | ||||
| -rw-r--r-- | tools/svc/proto/svc.swagger.json | 330 | ||||
| -rw-r--r-- | tools/svc/sample/health.lua | 16 | ||||
| -rw-r--r-- | tools/svc/sample/manifest.yaml | 28 | ||||
| -rw-r--r-- | tools/yk/main.go | 48 |
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 |
