├── .gitignore ├── Makefile ├── runtest.sh ├── .travis.yml ├── fixture └── id_rsa.pub ├── config_test.go ├── agent.go ├── models_test.go ├── LICENSE ├── keys_test.go ├── models.go ├── resin_test.go ├── keys.go ├── config.go ├── logs.go ├── application_test.go ├── application.go ├── env.go ├── README.md ├── device_test.go ├── resin.go └── device.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | ./runtest.sh 4 | 5 | test-device: 6 | ./runtest.sh run Device 7 | test-app: 8 | ./runtest.sh run Application 9 | test-keys: 10 | ./runtest.sh run Keys 11 | -------------------------------------------------------------------------------- /runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -e ./.env ];then 4 | echo "sourcing wnvironment vars from file" 5 | source .env 6 | #go test -v 7 | #else 8 | #go test -v 9 | fi 10 | case "$1" in 11 | "all") 12 | go test -v 13 | ;; 14 | "run") 15 | go test -v -run $2 16 | ;; 17 | *) 18 | go test -v 19 | esac 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | before_install: 5 | - go get -t -v ./... 6 | - go get github.com/axw/gocov/gocov 7 | - go get github.com/mattn/goveralls 8 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 9 | script: 10 | - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS 11 | -------------------------------------------------------------------------------- /fixture/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDhxl1OUQoBDQQbVgw34+VQ2mLQI+bZummqwl+w3HNjMl/woo4+qGjFwQVDymdUWqUqaSz4s2FnqoKUhGKlCC+AlJpQlanZGYLaLCHhtnupS/IAEptMUJnybQCbht4PjJQJnXp8f9zDTLHd6g9zVtI9PO/zKLMu0iTnlq9t6itSO3SRy/iwppJ9Xm47vrfRwY1tMQpxPngG+1Xi02VF9+Lf6BCiRNtN1/mcs2ACP+FeyXY+WU3FBlq7gshuFqZKezoeQObioWjQhLOEUxagMmyzZ9PvWVFvOs2VjLv7+ahcZjKmpXTgtAHNdow2+goYKpT8NRAHA/9hib4dZL/L3K7h gernest@MacBooks-MBP 2 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestResinConfig(t *testing.T) { 9 | config := &Config{ 10 | Username: ENV.Username, 11 | Password: ENV.Password, 12 | ResinEndpoint: apiEndpoint, 13 | } 14 | client := &http.Client{} 15 | ctx := &Context{ 16 | Client: client, 17 | Config: config, 18 | } 19 | err := Login(ctx, Credentials) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | _, err = ConfigGetAll(ctx) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /agent.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | //AgentReboot reboots the device 9 | func AgentReboot(ctx *Context, devID, appID int64, force bool) error { 10 | h := authHeader(ctx.Config.AuthToken) 11 | uri := apiEndpoint + "/supervisor/v1/reboot" 12 | data := make(map[string]interface{}) 13 | data["deviceId"] = devID 14 | data["appId"] = appID 15 | data["force"] = force 16 | body, err := marhsalReader(data) 17 | if err != nil { 18 | return err 19 | } 20 | b, err := doJSON(ctx, "POST", uri, h, nil, body) 21 | if err != nil { 22 | return err 23 | } 24 | var res = struct { 25 | Data string 26 | Error string 27 | }{} 28 | err = json.Unmarshal(b, &res) 29 | if err != nil { 30 | return err 31 | } 32 | if res.Data != "OK" { 33 | return errors.New("bad response :" + res.Error) 34 | } 35 | return nil 36 | 37 | } 38 | -------------------------------------------------------------------------------- /models_test.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import "testing" 4 | 5 | func TestDeviceType(t *testing.T) { 6 | sample := []struct { 7 | typ DeviceType 8 | expect string 9 | }{ 10 | {Artik10, "artik10"}, 11 | {Artik5, "artik5"}, 12 | {BeagleboneBlack, "beaglebone-black"}, 13 | {HumingBoard, "hummingboard"}, 14 | {IntelAdison, "intel-edison"}, 15 | {IntelNuc, "intel-nuc"}, 16 | {Nitrogen6x, "nitrogen6x"}, 17 | {OdroidC1, "odroid-c1"}, 18 | {OdroidXu4, "odroid-xu4"}, 19 | {Parallella, "parallella"}, 20 | {RaspberryPi, "raspberry-pi"}, 21 | {RaspberryPi2, "raspberry-pi2"}, 22 | {RaspberryPi3, "raspberrypi3"}, 23 | {Ts4900, "ts4900"}, 24 | {Ts700, "ts7700"}, 25 | {ViaVabx820Quad, "via-vab820-quad"}, 26 | {ZyncXz702, "zynq-xz702"}, 27 | } 28 | 29 | for _, v := range sample { 30 | if v.typ.String() != v.expect { 31 | t.Errorf("expetcted %s got %v", v.expect, v.typ) 32 | } 33 | } 34 | unkown := DeviceType(100) 35 | if unkown.String() != "Unknown" { 36 | t.Errorf("expected Unknown got %v", unkown) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Geofrey Ernest 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /keys_test.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestKeys(t *testing.T) { 10 | config := &Config{ 11 | Username: ENV.Username, 12 | Password: ENV.Password, 13 | ResinEndpoint: apiEndpoint, 14 | } 15 | client := &http.Client{} 16 | ctx := &Context{ 17 | Client: client, 18 | Config: config, 19 | } 20 | err := Login(ctx, Credentials) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | sample := []struct { 25 | title, file string 26 | key *Key 27 | }{ 28 | {"testKey", "fixture/id_rsa.pub", nil}, 29 | } 30 | t.Run("Create", func(ts *testing.T) { 31 | for i, v := range sample { 32 | d, err := ioutil.ReadFile(v.file) 33 | if err != nil { 34 | ts.Fatal(err) 35 | } 36 | k, err := KeyCreate(ctx, ctx.Config.UserID(), string(d), v.title) 37 | if err != nil { 38 | ts.Fatal(err) 39 | } 40 | sample[i].key = k 41 | } 42 | }) 43 | t.Run("GetByID", func(ts *testing.T) { 44 | for _, v := range sample { 45 | k, err := KeyGetByID(ctx, v.key.ID) 46 | if err != nil { 47 | ts.Fatal(err) 48 | } 49 | if k.Title != v.title { 50 | ts.Errorf("expected %s got %s", v.title, k.Title) 51 | } 52 | } 53 | }) 54 | t.Run("GetAll", func(ts *testing.T) { 55 | keys, err := KeyGetAll(ctx) 56 | if err != nil { 57 | ts.Fatal(err) 58 | } 59 | if len(keys) < 1 { 60 | ts.Error("expected at least one key") 61 | } 62 | }) 63 | t.Run("Remove", func(ts *testing.T) { 64 | for _, v := range sample { 65 | err := KeyRemove(ctx, v.key.ID) 66 | if err != nil { 67 | ts.Fatal(err) 68 | } 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | //DeviceType is the identity of the the device that is supported by resin. 4 | type DeviceType int 5 | 6 | // supported devices 7 | const ( 8 | Artik10 DeviceType = iota 9 | Artik5 10 | BeagleboneBlack 11 | HumingBoard 12 | IntelAdison 13 | IntelNuc 14 | Nitrogen6x 15 | OdroidC1 16 | OdroidXu4 17 | Parallella 18 | RaspberryPi 19 | RaspberryPi2 20 | RaspberryPi3 21 | Ts4900 22 | Ts700 23 | ViaVabx820Quad 24 | ZyncXz702 25 | ) 26 | 27 | func (d DeviceType) String() string { 28 | switch d { 29 | case Artik10: 30 | return "artik10" 31 | case Artik5: 32 | return "artik5" 33 | case BeagleboneBlack: 34 | return "beaglebone-black" 35 | case HumingBoard: 36 | return "hummingboard" 37 | case IntelAdison: 38 | return "intel-edison" 39 | case IntelNuc: 40 | return "intel-nuc" 41 | case Nitrogen6x: 42 | return "nitrogen6x" 43 | case OdroidC1: 44 | return "odroid-c1" 45 | case OdroidXu4: 46 | return "odroid-xu4" 47 | case Parallella: 48 | return "parallella" 49 | case RaspberryPi: 50 | return "raspberry-pi" 51 | case RaspberryPi2: 52 | return "raspberry-pi2" 53 | case RaspberryPi3: 54 | return "raspberrypi3" 55 | case Ts4900: 56 | return "ts4900" 57 | case Ts700: 58 | return "ts7700" 59 | case ViaVabx820Quad: 60 | return "via-vab820-quad" 61 | case ZyncXz702: 62 | return "zynq-xz702" 63 | } 64 | return "Unknown" 65 | } 66 | 67 | //Repository is a resin remote repository 68 | type Repository struct { 69 | URL string 70 | Commit string 71 | } 72 | 73 | //User a resin user 74 | type User struct { 75 | ID int64 `json:"__id"` 76 | Metadata struct { 77 | URI string `json:"uri"` 78 | } `json:"__deferred"` 79 | } 80 | -------------------------------------------------------------------------------- /resin_test.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "os" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | var ENV *EnvVars 12 | 13 | type EnvVars struct { 14 | Username, Email, Password string 15 | ID int64 16 | Register struct { 17 | Username, Email, Password string 18 | } 19 | } 20 | 21 | func init() { 22 | ENV = &EnvVars{ 23 | Username: os.Getenv("RESINTEST_USERNAME"), 24 | Password: os.Getenv("RESINTEST_PASSWORD"), 25 | Email: os.Getenv("RESINTEST_EMAIL"), 26 | } 27 | ENV.Register.Username = os.Getenv("RESINTEST_REGISTER_USERNAME") 28 | ENV.Register.Password = os.Getenv("RESINTEST_REGISTER_PASSWORD") 29 | ENV.Register.Email = os.Getenv("RESINTEST_REGISTER_EMAIL") 30 | } 31 | 32 | func TestResin(t *testing.T) { 33 | config := &Config{ 34 | Username: ENV.Username, 35 | Password: ENV.Password, 36 | ResinEndpoint: apiEndpoint, 37 | } 38 | client := &http.Client{} 39 | t.Run("Authenticate", func(ts *testing.T) { 40 | cfg := *config 41 | ctx := &Context{ 42 | Client: client, 43 | Config: &cfg, 44 | } 45 | testAuthenticate(ctx, ts) 46 | }) 47 | t.Run("Login", func(ts *testing.T) { 48 | cfg := *config 49 | ctx := &Context{ 50 | Client: client, 51 | Config: &cfg, 52 | } 53 | testLogin(ctx, ts) 54 | }) 55 | } 56 | 57 | func testAuthenticate(ctx *Context, t *testing.T) { 58 | token, err := Authenticate(ctx, Credentials) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | t.Run("ParseToken", func(ts *testing.T) { 63 | claims, err := ParseToken(token) 64 | if err != nil { 65 | ts.Fatal(err) 66 | } 67 | if claims.Username != ctx.Config.Username { 68 | ts.Errorf("expected username %s got %s", ctx.Config.Username, claims.Username) 69 | } 70 | 71 | }) 72 | } 73 | 74 | func testLogin(ctx *Context, t *testing.T) { 75 | err := Login(ctx, Credentials) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if ctx.Config.tokenClain == nil { 80 | t.Error("expected the token to be saved") 81 | } 82 | } 83 | 84 | func TestEncode(t *testing.T) { 85 | sample := []struct { 86 | params []string 87 | expect string 88 | }{ 89 | {[]string{"filter,Name", "eq,Milk"}, "$filter=Name%20eq%20'Milk'"}, 90 | {[]string{"expand,device"}, "$expand=device"}, 91 | } 92 | 93 | for _, v := range sample { 94 | param := make(url.Values) 95 | for _, p := range v.params { 96 | s := strings.Split(p, ",") 97 | param.Set(s[0], s[1]) 98 | } 99 | e := Encode(param) 100 | if e != v.expect { 101 | t.Errorf("expectes %s got %s", v.expect, e) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | //Key is a user public key on resin 11 | type Key struct { 12 | ID int64 `json:"id"` 13 | Title string `json:"title"` 14 | PublicKey string `json:"public_key"` 15 | User struct { 16 | ID int64 `json:"__id"` 17 | Deferred struct { 18 | URI string `json:"uri"` 19 | } `json:"__deferred"` 20 | } `json:"user"` 21 | Metadata struct { 22 | URI string `json:"uri"` 23 | Type string `json:"type"` 24 | } `json:"__metadata"` 25 | CreatedAt time.Time `json:"created_at"` 26 | } 27 | 28 | //KeyGetAll retrives all key for the user who authenticated ctx. 29 | func KeyGetAll(ctx *Context) ([]*Key, error) { 30 | h := authHeader(ctx.Config.AuthToken) 31 | uri := ctx.Config.APIEndpoint("user__has__public_key") 32 | b, err := doJSON(ctx, "GET", uri, h, nil, nil) 33 | if err != nil { 34 | return nil, err 35 | } 36 | res := struct { 37 | D []*Key `json:"d"` 38 | }{} 39 | err = json.Unmarshal(b, &res) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return res.D, nil 44 | } 45 | 46 | //KeyGetByID retrives public key with the given id 47 | func KeyGetByID(ctx *Context, id int64) (*Key, error) { 48 | h := authHeader(ctx.Config.AuthToken) 49 | s := fmt.Sprintf("user__has__public_key(%d)", id) 50 | uri := ctx.Config.APIEndpoint(s) 51 | b, err := doJSON(ctx, "GET", uri, h, nil, nil) 52 | if err != nil { 53 | return nil, err 54 | } 55 | res := struct { 56 | D []*Key `json:"d"` 57 | }{} 58 | err = json.Unmarshal(b, &res) 59 | if err != nil { 60 | return nil, err 61 | } 62 | if len(res.D) > 0 { 63 | return res.D[0], nil 64 | } 65 | return nil, errors.New("key not found") 66 | } 67 | 68 | //KeyCreate creates a public key for the user with given userID 69 | func KeyCreate(ctx *Context, userID int64, key, title string) (*Key, error) { 70 | h := authHeader(ctx.Config.AuthToken) 71 | uri := ctx.Config.APIEndpoint("user__has__public_key") 72 | data := make(map[string]interface{}) 73 | data["user"] = userID 74 | data["public_key"] = key 75 | data["title"] = title 76 | body, err := marhsalReader(data) 77 | if err != nil { 78 | return nil, err 79 | //return nil 80 | } 81 | b, err := doJSON(ctx, "POST", uri, h, nil, body) 82 | if err != nil { 83 | return nil, err 84 | //return nil 85 | } 86 | //fmt.Println(string(b)) 87 | e := &Key{} 88 | err = json.Unmarshal(b, e) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return e, nil 93 | //return nil 94 | } 95 | 96 | //KeyRemove removes the public key with the given id 97 | func KeyRemove(ctx *Context, id int64) error { 98 | h := authHeader(ctx.Config.AuthToken) 99 | s := fmt.Sprintf("user__has__public_key(%d)", id) 100 | uri := ctx.Config.APIEndpoint(s) 101 | b, err := doJSON(ctx, "DELETE", uri, h, nil, nil) 102 | if err != nil { 103 | return err 104 | } 105 | if string(b) != "OK" { 106 | return errors.New("bad response") 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import "encoding/json" 4 | 5 | //ResinConfig resin configuration 6 | type ResinConfig struct { 7 | SocialProviders []string `json:"supportedSocialProviders"` 8 | SignupCodeRequired bool `json:"signupCodeRequired"` 9 | MixPanelToken string `json:"mixpanelToken"` 10 | KeenProjectID string `json:"keenProjectId"` 11 | KeenReadKey string `json:"keenReadKey"` 12 | DeviceURLBase string `json:"deviceUrlsBase"` 13 | GitServerURL string `json:"gitServerUrl"` 14 | ImageMakerURL string `json:"imgMakerUrl"` 15 | AdminURL string `json:"adminUrl"` 16 | DebugEnabled bool `json:"debugEnabled"` 17 | PubNub struct { 18 | PubKey string `json:"publish_key"` 19 | SubKey string `json:"subscribe_key"` 20 | } `json:"pubnub"` 21 | GoogleAnalytics struct { 22 | ID string `kson:"id"` 23 | Site string `json:"site"` 24 | } `json:"ga"` 25 | 26 | DeviceTypes []struct { 27 | Slug string `json:"slug"` 28 | Version int `json:"version"` 29 | Aliases []string `json:"aliases"` 30 | Name string `json:"name"` 31 | Arch string `json:"arch"` 32 | State string `json:"state"` 33 | StateInstructions struct { 34 | PostProvisioning []string `json:"postProvisioning"` 35 | } `json:"stateInstructions"` 36 | //Instructions []string `json:"instructions"` 37 | SupportsBlink bool `json:"supportsBlink"` 38 | Yocto struct { 39 | Machine string `json:"machine"` 40 | Image string `json:"image"` 41 | FSType string `json:"fstype"` 42 | Version string `json:"version"` 43 | DeployArtfact string `json:"deployArtfact"` 44 | Compressed bool `json:"compressed"` 45 | } `json:"yocto"` 46 | Options []struct { 47 | IsGroup bool `json:"isGroup"` 48 | Name string `json:"name"` 49 | Message string `json:"message"` 50 | Options []struct { 51 | Name string `json:"name"` 52 | Message string `json:"message"` 53 | Type string `json:"type"` 54 | CHoices []string `json:"choices"` 55 | } `json:"options"` 56 | } `json:"options"` 57 | Configuration struct { 58 | Config struct { 59 | Partition struct { 60 | Primary int `json:"primary"` 61 | } `json:"partition"` 62 | } `json:"config"` 63 | } `json:"configuration"` 64 | Initialization struct { 65 | Options []struct { 66 | Name string `json:"name"` 67 | Message string `json:"message"` 68 | Type string `json:"type"` 69 | } `json:"options"` 70 | Operations []struct { 71 | Command string `json:"command"` 72 | } `json:"operations"` 73 | } `json:"initialization"` 74 | BuildID string `json:"buildId"` 75 | } `json:"deviceTypes"` 76 | } 77 | 78 | //ConfigGetAll return resin congiguration 79 | func ConfigGetAll(ctx *Context) (*ResinConfig, error) { 80 | h := authHeader(ctx.Config.AuthToken) 81 | //uri := ctx.Config.APIEndpoint("config") 82 | uri := apiEndpoint + "/config" 83 | b, err := doJSON(ctx, "GET", uri, h, nil, nil) 84 | if err != nil { 85 | return nil, err 86 | } 87 | cfg := &ResinConfig{} 88 | err = json.Unmarshal(b, cfg) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return cfg, nil 93 | } 94 | -------------------------------------------------------------------------------- /logs.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/antonholmquist/jason" 9 | "github.com/pubnub/go/messaging" 10 | ) 11 | 12 | //Logs streams resin device logs 13 | // 14 | // Resin uses pubnub service for logs. Unfortunate the API for pubnub sucks big 15 | // time. This Limits the API for this struct too. 16 | type Logs struct { 17 | nub *messaging.Pubnub 18 | channel string 19 | ctx *Context 20 | stop chan struct{} 21 | } 22 | 23 | //NewLogs returns a new Logs instace which is initialized to support streaming 24 | //logs from pubnub. 25 | func NewLogs(ctx *Context) (*Logs, error) { 26 | cfg, err := ConfigGetAll(ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | //pretty.Println(cfg) 31 | if cfg.PubNub.PubKey != "" && cfg.PubNub.SubKey != "" { 32 | n := messaging.NewPubnub( 33 | cfg.PubNub.PubKey, 34 | cfg.PubNub.SubKey, "", "", false, "", 35 | ) 36 | return &Logs{nub: n, ctx: ctx, stop: make(chan struct{})}, nil 37 | } 38 | return nil, errors.New("resingo: no pubnub details found") 39 | } 40 | 41 | //Subscribe subscribe to device logs 42 | func (l *Logs) Subscribe(uuid string) ( 43 | chan []byte, chan []byte, error, 44 | ) { 45 | logChan, err := l.GetChannel(uuid) 46 | if err != nil { 47 | return nil, nil, err 48 | } 49 | schan, echan := messaging.CreateSubscriptionChannels() 50 | l.nub.Subscribe(logChan, "", schan, false, echan) 51 | return schan, echan, nil 52 | 53 | } 54 | 55 | //GetChannel returns the device logs channel for the device with given uuid.. 56 | //The value is not cached, so this is stateless. This allows to use the same 57 | //Lofs instance to syvscribe to multiple devices in different goroutines without 58 | //race conditions. 59 | func (l *Logs) GetChannel(uuid string) (string, error) { 60 | logsChan := uuid 61 | dev, err := DevGetByUUID(l.ctx, uuid) 62 | if err != nil { 63 | return "", err 64 | } 65 | if dev.LogsChannel != "" { 66 | logsChan = dev.LogsChannel 67 | } 68 | return fmt.Sprintf("device-%s-logs", logsChan), nil 69 | } 70 | 71 | //Log streams logs go out. This is blocking opretation, you should run this in a 72 | //gorouting and call Log.Close when you are done acceping writes to out. 73 | func (l *Logs) Log(uuid string, out io.Writer) error { 74 | s, e, err := l.Subscribe(uuid) 75 | if err != nil { 76 | return err 77 | } 78 | stop: 79 | for { 80 | select { 81 | case rcv := <-s: 82 | nerr := l.write(out, rcv) 83 | if nerr != nil { 84 | fmt.Println(nerr) 85 | } 86 | case errrcv := <-e: 87 | err = errors.New(string(errrcv)) 88 | break stop 89 | case <-l.stop: 90 | fmt.Println("resingo: stopping streaming logs") 91 | break stop 92 | } 93 | } 94 | l.nub.Abort() 95 | return err 96 | } 97 | 98 | func (l *Logs) write(out io.Writer, src []byte) error { 99 | a, _, _, err := l.nub.ParseJSON(src, "") 100 | if err != nil { 101 | return err 102 | } 103 | v, err := jason.NewValueFromBytes([]byte(a)) 104 | if err != nil { 105 | return err 106 | } 107 | va, err := v.Array() 108 | if err != nil { 109 | return err 110 | } 111 | for _, value := range va { 112 | na, err := value.ObjectArray() 113 | if err != nil { 114 | return err 115 | } 116 | for _, vn := range na { 117 | m, err := vn.GetString("m") 118 | if err != nil { 119 | return err 120 | } 121 | fmt.Fprintf(out, " %s \n", m) 122 | } 123 | } 124 | return nil 125 | } 126 | 127 | //Close stops streaming device logs. 128 | func (l *Logs) Close() { 129 | l.stop <- struct{}{} 130 | } 131 | -------------------------------------------------------------------------------- /application_test.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestApplication(t *testing.T) { 10 | config := &Config{ 11 | Username: ENV.Username, 12 | Password: ENV.Password, 13 | ResinEndpoint: apiEndpoint, 14 | } 15 | client := &http.Client{} 16 | ctx := &Context{ 17 | Client: client, 18 | Config: config, 19 | } 20 | err := Login(ctx, Credentials) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | applications := []struct { 25 | name string 26 | app *Application 27 | typ DeviceType 28 | }{ 29 | {"resingo", nil, RaspberryPi3}, 30 | {"algorithm_zero", nil, RaspberryPi2}, 31 | } 32 | for i, a := range applications { 33 | app, err := AppCreate(ctx, a.name, a.typ) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | applications[i].app = app 38 | } 39 | defer func() { 40 | for _, a := range applications { 41 | _, _ = AppDelete(ctx, a.app.ID) 42 | } 43 | }() 44 | 45 | t.Run("AppGetAll", func(ts *testing.T) { 46 | testAppGetAll(ctx, ts) 47 | }) 48 | t.Run("AppGetByName", func(ts *testing.T) { 49 | for _, a := range applications { 50 | testApGetByName(ctx, ts, a.name) 51 | } 52 | }) 53 | t.Run("AppGetByID", func(ts *testing.T) { 54 | for _, a := range applications { 55 | testApGetByID(ctx, ts, a.app.ID) 56 | } 57 | }) 58 | t.Run("AppCreate", func(ts *testing.T) { 59 | testAppCreate(ctx, ts, "resingo_test", RaspberryPi3) 60 | }) 61 | t.Run("GetApiKey", func(ts *testing.T) { 62 | for _, a := range applications { 63 | testAppAPIKey(ctx, ts, a.name) 64 | } 65 | }) 66 | env := []struct { 67 | key, value string 68 | }{ 69 | {"ToALL", "Programmers"}, 70 | {"Around", "TheWorld"}, 71 | } 72 | t.Run("CreateEnv", func(ts *testing.T) { 73 | for _, v := range applications { 74 | for _, e := range env { 75 | en, err := EnvAppCreate(ctx, v.app.ID, e.key, e.value) 76 | if err != nil { 77 | ts.Error(err) 78 | } 79 | if en.Name != e.key { 80 | ts.Errorf("expected %s got %s", e.key, en.Name) 81 | } 82 | } 83 | } 84 | }) 85 | t.Run("EnvGetAll", func(ts *testing.T) { 86 | for _, v := range applications { 87 | envs, err := EnvAppGetAll(ctx, v.app.ID) 88 | if err != nil { 89 | ts.Error(err) 90 | } 91 | if len(envs) != len(env) { 92 | ts.Errorf("expected %d got %d", len(env), len(envs)) 93 | } 94 | } 95 | }) 96 | t.Run("EnvUpdate", func(ts *testing.T) { 97 | envs, err := EnvAppGetAll(ctx, applications[0].app.ID) 98 | if err != nil { 99 | ts.Fatal(err) 100 | } 101 | for _, e := range envs { 102 | err := EnvAppUpdate(ctx, e.ID, e.Name) 103 | if err != nil { 104 | ts.Error(err) 105 | } 106 | } 107 | }) 108 | t.Run("EnvDelete", func(ts *testing.T) { 109 | envs, err := EnvAppGetAll(ctx, applications[0].app.ID) 110 | if err != nil { 111 | ts.Fatal(err) 112 | } 113 | for _, e := range envs { 114 | err := EnvAppDelete(ctx, e.ID) 115 | if err != nil { 116 | ts.Error(err) 117 | } 118 | } 119 | }) 120 | } 121 | 122 | func testAppGetAll(ctx *Context, t *testing.T) { 123 | apps, err := AppGetAll(ctx) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | if len(apps) == 0 { 128 | t.Fatal("expected at least more than one application") 129 | } 130 | } 131 | 132 | func testApGetByName(ctx *Context, t *testing.T, name string) { 133 | app, err := AppGetByName(ctx, name) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | if app.Name != name { 138 | t.Errorf("expected %s got %s %v", name, app.Name, *app) 139 | } 140 | } 141 | func testApGetByID(ctx *Context, t *testing.T, id int64) { 142 | app, err := AppGetByID(ctx, id) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | if app.ID != id { 147 | t.Errorf("expected %d got %d", id, app.ID) 148 | } 149 | } 150 | 151 | func testAppCreate(ctx *Context, t *testing.T, name string, typ DeviceType) { 152 | app, err := AppCreate(ctx, name, typ) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | if app.Name != name { 157 | t.Fatalf("expected %s got %s", name, app.Name) 158 | } 159 | t.Run("Delete", func(ts *testing.T) { 160 | testAppDelete(ctx, ts, app.ID) 161 | }) 162 | } 163 | 164 | func testAppDelete(ctx *Context, t *testing.T, id int64) { 165 | _, err := AppDelete(ctx, id) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | _, err = AppGetByID(ctx, id) 170 | if err == nil { 171 | t.Error("expected devcice not found error") 172 | } 173 | } 174 | func testAppAPIKey(ctx *Context, t *testing.T, name string) { 175 | b, err := AppGetAPIKey(ctx, name) 176 | if err != nil { 177 | t.Error(err) 178 | } 179 | fmt.Println(string(b)) 180 | } 181 | -------------------------------------------------------------------------------- /application.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | ) 13 | 14 | //Application holds information about the application that is running on resin. 15 | type Application struct { 16 | ID int64 `json:"id"` 17 | Name string `json:"app_name"` 18 | Repository string `json:"git_repository"` 19 | Metadata struct { 20 | URI string `json:"uri"` 21 | Type string `json:"type"` 22 | } `json:"__metadata"` 23 | DeviceType string `json:"device_type"` 24 | User User `json:"user"` 25 | Commit string `json:"commit"` 26 | } 27 | 28 | //AppGetAll retrieves all applications that belog to the user in the given 29 | //context. 30 | // 31 | // For this to work, the context should be authorized, probably using the Login 32 | // function. 33 | func AppGetAll(ctx *Context) ([]*Application, error) { 34 | h := authHeader(ctx.Config.AuthToken) 35 | uri := ctx.Config.APIEndpoint("application") 36 | b, err := doJSON(ctx, "GET", uri, h, nil, nil) 37 | if err != nil { 38 | return nil, err 39 | } 40 | var appRes = struct { 41 | D []*Application `json:"d"` 42 | }{} 43 | err = json.Unmarshal(b, &appRes) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return appRes.D, nil 48 | } 49 | 50 | //AppGetByName returns the application with the giveb name. 51 | func AppGetByName(ctx *Context, name string) (*Application, error) { 52 | h := authHeader(ctx.Config.AuthToken) 53 | uri := ctx.Config.APIEndpoint("application") 54 | params := make(url.Values) 55 | params.Set("filter", "app_name") 56 | params.Set("eq", name) 57 | b, err := doJSON(ctx, "GET", uri, h, params, nil) 58 | if err != nil { 59 | return nil, err 60 | } 61 | var appRes = struct { 62 | D []*Application `json:"d"` 63 | }{} 64 | err = json.Unmarshal(b, &appRes) 65 | if err != nil { 66 | return nil, err 67 | } 68 | if len(appRes.D) > 0 { 69 | return appRes.D[0], nil 70 | } 71 | return nil, errors.New("application not found") 72 | } 73 | 74 | //The client expects a 200 status code for a successful reqest, any 75 | // other status code will result into an error with the form [code] [ Any 76 | // content read from the response body] 77 | func do(ctx *Context, method, uri string, header http.Header, 78 | params url.Values, body io.Reader) ([]byte, error) { 79 | if params != nil { 80 | uri = uri + "?" + Encode(params) 81 | } 82 | req, err := http.NewRequest(method, uri, body) 83 | if err != nil { 84 | return nil, err 85 | } 86 | if header != nil { 87 | req.Header = header 88 | } 89 | req.Header = header 90 | resp, err := ctx.Client.Do(req) 91 | if err != nil { 92 | return nil, err 93 | } 94 | defer func() { 95 | _ = resp.Body.Close() 96 | }() 97 | b, err := ioutil.ReadAll(resp.Body) 98 | if err != nil { 99 | return nil, err 100 | } 101 | if !checkStatus(resp.StatusCode) { 102 | return nil, fmt.Errorf("resingo: [%d ] %s : %s", resp.StatusCode, req.URL.RequestURI(), string(b)) 103 | } 104 | return b, nil 105 | } 106 | 107 | func checkStatus(status int) bool { 108 | switch status { 109 | case http.StatusOK, http.StatusCreated, http.StatusAccepted: 110 | return true 111 | } 112 | return false 113 | } 114 | 115 | func doJSON(ctx *Context, method, uri string, header http.Header, 116 | params url.Values, body io.Reader) ([]byte, error) { 117 | header.Set("Content-Type", "application/json") 118 | return do(ctx, method, uri, header, params, body) 119 | } 120 | 121 | //AppGetByID returns application with the given id 122 | func AppGetByID(ctx *Context, id int64) (*Application, error) { 123 | h := authHeader(ctx.Config.AuthToken) 124 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("application(%d)", id)) 125 | b, err := doJSON(ctx, "GET", uri, h, nil, nil) 126 | if err != nil { 127 | return nil, err 128 | } 129 | var appRes = struct { 130 | D []*Application `json:"d"` 131 | }{} 132 | err = json.Unmarshal(b, &appRes) 133 | if err != nil { 134 | return nil, err 135 | } 136 | if len(appRes.D) > 0 { 137 | return appRes.D[0], nil 138 | } 139 | return nil, errors.New("application not found") 140 | } 141 | 142 | //AppCreate creates a new application with the given name 143 | func AppCreate(ctx *Context, name string, typ DeviceType) (*Application, error) { 144 | h := authHeader(ctx.Config.AuthToken) 145 | uri := ctx.Config.APIEndpoint("application") 146 | data := make(map[string]interface{}) 147 | data["app_name"] = name 148 | data["device_type"] = typ.String() 149 | body, err := marhsalReader(data) 150 | if err != nil { 151 | return nil, err 152 | } 153 | b, err := doJSON(ctx, "POST", uri, h, nil, body) 154 | if err != nil { 155 | return nil, err 156 | } 157 | rst := &Application{} 158 | err = json.Unmarshal(b, rst) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return rst, nil 163 | } 164 | 165 | func marhsalReader(o interface{}) (io.Reader, error) { 166 | b, err := json.Marshal(o) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return bytes.NewReader(b), nil 171 | } 172 | 173 | //AppDelete removes the application with the given id 174 | func AppDelete(ctx *Context, id int64) (bool, error) { 175 | h := authHeader(ctx.Config.AuthToken) 176 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("application(%d)", id)) 177 | b, err := doJSON(ctx, "DELETE", uri, h, nil, nil) 178 | if err != nil { 179 | return false, err 180 | } 181 | return string(b) == "OK", nil 182 | } 183 | 184 | //AppGetAPIKey returns the application with the given api key 185 | func AppGetAPIKey(ctx *Context, name string) ([]byte, error) { 186 | h := authHeader(ctx.Config.AuthToken) 187 | app, err := AppGetByName(ctx, name) 188 | if err != nil { 189 | return nil, err 190 | } 191 | end := fmt.Sprintf("application/%d/generate-api-key", app.ID) 192 | uri := "https://api.resin.io/" + end 193 | return doJSON(ctx, "POST", uri, h, nil, nil) 194 | } 195 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | //Env contains the response for device environment variable 11 | type Env struct { 12 | ID int64 `json:"id"` 13 | Name string `json:"env_var_name"` 14 | Value string `json:"value"` 15 | Device struct { 16 | ID int64 `json:"__id"` 17 | Deferred struct { 18 | URI string `json:"uri"` 19 | } `json:"__deferred"` 20 | } `json:"device"` 21 | Metadata struct { 22 | URI string `json:"uri"` 23 | Type string `json:"type"` 24 | } `json:"__metadata"` 25 | } 26 | 27 | //AppEnv application environment variable 28 | type AppEnv struct { 29 | ID int64 `json:"id"` 30 | Name string `json:"name"` 31 | Value string `json:"value"` 32 | Application struct { 33 | ID int64 `json:"__id"` 34 | Deferred struct { 35 | URI string `json:"uri"` 36 | } `json:"__deferred"` 37 | } `json:"application"` 38 | Metadata struct { 39 | URI string `json:"uri"` 40 | Type string `json:"type"` 41 | } `json:"__metadata"` 42 | } 43 | 44 | //EnvDevCreate creates environment variable for the device 45 | func EnvDevCreate(ctx *Context, id int64, key, value string) (*Env, error) { 46 | h := authHeader(ctx.Config.AuthToken) 47 | uri := ctx.Config.APIEndpoint("device_environment_variable") 48 | data := make(map[string]interface{}) 49 | data["device"] = id 50 | data["env_var_name"] = key 51 | data["value"] = value 52 | body, err := marhsalReader(data) 53 | if err != nil { 54 | return nil, err 55 | } 56 | b, err := doJSON(ctx, "POST", uri, h, nil, body) 57 | if err != nil { 58 | return nil, err 59 | } 60 | e := &Env{} 61 | err = json.Unmarshal(b, e) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return e, nil 66 | } 67 | 68 | //EnvDevGetAll get all environment variables for the device 69 | func EnvDevGetAll(ctx *Context, id int64) ([]*Env, error) { 70 | h := authHeader(ctx.Config.AuthToken) 71 | uri := ctx.Config.APIEndpoint("device_environment_variable") 72 | param := make(url.Values) 73 | param.Set("filter", "device") 74 | param.Set("eq", fmt.Sprint(id)) 75 | b, err := doJSON(ctx, "GET", uri, h, param, nil) 76 | if err != nil { 77 | return nil, err 78 | } 79 | res := struct { 80 | D []*Env `json:"d"` 81 | }{} 82 | err = json.Unmarshal(b, &res) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return res.D, nil 87 | } 88 | 89 | //EnvDevUpdate updates environment variable for device. The id is for the 90 | //environmant variable. 91 | func EnvDevUpdate(ctx *Context, id int64, value string) error { 92 | h := authHeader(ctx.Config.AuthToken) 93 | s := fmt.Sprintf("device_environment_variable(%d)", id) 94 | uri := ctx.Config.APIEndpoint(s) 95 | data := make(map[string]interface{}) 96 | data["value"] = value 97 | body, err := marhsalReader(data) 98 | if err != nil { 99 | return err 100 | } 101 | b, err := doJSON(ctx, "PATCH", uri, h, nil, body) 102 | if err != nil { 103 | return err 104 | } 105 | if string(b) != "OK" { 106 | return errors.New("bad response") 107 | } 108 | return nil 109 | } 110 | 111 | //EnvDevDelete deketes device environment variable 112 | func EnvDevDelete(ctx *Context, id int64) error { 113 | h := authHeader(ctx.Config.AuthToken) 114 | s := fmt.Sprintf("device_environment_variable(%d)", id) 115 | uri := ctx.Config.APIEndpoint(s) 116 | b, err := doJSON(ctx, "DELETE", uri, h, nil, nil) 117 | if err != nil { 118 | return err 119 | } 120 | if string(b) != "OK" { 121 | return errors.New("bad response") 122 | } 123 | return nil 124 | } 125 | 126 | //EnvAppGetAll retruns all environment variables for application 127 | func EnvAppGetAll(ctx *Context, id int64) ([]*AppEnv, error) { 128 | h := authHeader(ctx.Config.AuthToken) 129 | uri := ctx.Config.APIEndpoint("environment_variable") 130 | param := make(url.Values) 131 | param.Set("filter", "application") 132 | param.Set("eq", fmt.Sprint(id)) 133 | b, err := doJSON(ctx, "GET", uri, h, param, nil) 134 | if err != nil { 135 | return nil, err 136 | } 137 | res := struct { 138 | D []*AppEnv `json:"d"` 139 | }{} 140 | err = json.Unmarshal(b, &res) 141 | if err != nil { 142 | return nil, err 143 | } 144 | return res.D, nil 145 | } 146 | 147 | //EnvAppCreate creates a newapplication environment variable 148 | func EnvAppCreate(ctx *Context, id int64, key, value string) (*AppEnv, error) { 149 | h := authHeader(ctx.Config.AuthToken) 150 | uri := ctx.Config.APIEndpoint("environment_variable") 151 | data := make(map[string]interface{}) 152 | data["application"] = id 153 | data["name"] = key 154 | data["value"] = value 155 | body, err := marhsalReader(data) 156 | if err != nil { 157 | return nil, err 158 | } 159 | b, err := doJSON(ctx, "POST", uri, h, nil, body) 160 | if err != nil { 161 | return nil, err 162 | } 163 | e := &AppEnv{} 164 | err = json.Unmarshal(b, e) 165 | if err != nil { 166 | return nil, err 167 | } 168 | return e, nil 169 | } 170 | 171 | //EnvAppUpdate updates an existing application environmant variable 172 | func EnvAppUpdate(ctx *Context, id int64, value string) error { 173 | h := authHeader(ctx.Config.AuthToken) 174 | s := fmt.Sprintf("environment_variable(%d)", id) 175 | uri := ctx.Config.APIEndpoint(s) 176 | data := make(map[string]interface{}) 177 | data["value"] = value 178 | body, err := marhsalReader(data) 179 | if err != nil { 180 | return err 181 | } 182 | b, err := doJSON(ctx, "PATCH", uri, h, nil, body) 183 | if err != nil { 184 | return err 185 | } 186 | if string(b) != "OK" { 187 | return errors.New("bad response") 188 | } 189 | return nil 190 | } 191 | 192 | //EnvAppDelete deletes application environment variable 193 | func EnvAppDelete(ctx *Context, id int64) error { 194 | h := authHeader(ctx.Config.AuthToken) 195 | s := fmt.Sprintf("environment_variable(%d)", id) 196 | uri := ctx.Config.APIEndpoint(s) 197 | b, err := doJSON(ctx, "DELETE", uri, h, nil, nil) 198 | if err != nil { 199 | return err 200 | } 201 | if string(b) != "OK" { 202 | return errors.New("bad response") 203 | } 204 | return nil 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # resingo [![GoDoc](https://godoc.org/github.com/gernest/resingo?status.svg)](https://godoc.org/github.com/gernest/resingo) [![Go Report Card](https://goreportcard.com/badge/github.com/gernest/resingo)](https://goreportcard.com/report/github.com/gernest/resingo)[![Build Status](https://travis-ci.org/gernest/resingo.svg?branch=master)](https://travis-ci.org/gernest/resingo)[![Coverage Status](https://coveralls.io/repos/github/gernest/resingo/badge.svg?branch=master)](https://coveralls.io/github/gernest/resingo?branch=master) 2 | 3 | The unofficial golang sdk for resin.io 4 | 5 | **MAINTAINER WANTED** 6 | I no longer work with resin devices, so I don't have time to put into this. If 7 | you are interested in maintaining this please let me know! Thanks. 8 | 9 | ## what is resin and what is resingo? 10 | [Resin](https://resin.io/) is a service that brings the benefits of linux 11 | containers to IoT. It enables iterative development, deployment and management 12 | of devices at scale. Please go to their website for more information. 13 | 14 | Resingo, is a standard development kit for resin, that uses the Go(Golang) 15 | programming language. This library, provides nuts and bolts necessary for 16 | developers to interact with resin service using the Go programming language. 17 | 18 | This library is full featured, well tested and well designed. If you find 19 | anything that is missing please [open an issssue](https://github.com/gernest/resingo/issues) 20 | 21 | 22 | 23 | This is the laundry list of things you can do. 24 | 25 | - Applications 26 | - [x] Get all applications 27 | - [x] Get application by name 28 | - [x] Get application by application id 29 | - [x] Create Application 30 | - [x] Delete application 31 | - [x] Generate application API key 32 | - Devices 33 | - [x] Get all devices 34 | - [x] Get all devices for a given device name 35 | - [x] Get device by UUID 36 | - [x] Get device by name 37 | - [x] Get application name by device uuid 38 | - [x] Check if the device is online or not 39 | - [x] Get local IP address of the device 40 | - [x] Remove/Delete device 41 | - [x] Rename device 42 | - [x] Note a device 43 | - [x] Generate device UUID 44 | - [x] Register device 45 | - [x] Enable device url 46 | - [x] Disable device url 47 | - [x] Move device 48 | - [x] Check device status 49 | - [x] Identify device by blinkig 50 | 51 | - Environment 52 | - Device 53 | - [x] Get all device environment variables 54 | - [x] Create a device environment variable 55 | - [x] Update device environment variable 56 | - [x] Remove/Delete device environment variable 57 | - Application 58 | - [x] Get all application environment variables 59 | - [x] Create application environment variable 60 | - [x] Update application environment variable 61 | - [x] Remove application environment variable 62 | 63 | - Keys 64 | - [x] Get all ssh keys 65 | - [x] Get a dingle ssh key 66 | - [x] Remove ssh key 67 | - [x] Create ssh key 68 | 69 | - Os 70 | - [ ] Download Os Image 71 | 72 | - Config 73 | - [x] Get all configurations 74 | 75 | - Logs 76 | - [x] Subscribe to device logs 77 | - [ ] Retrieve historical logs 78 | 79 | - Supervisor 80 | - [x] Reboot 81 | 82 | # Introduction 83 | 84 | ## Installation 85 | 86 | ```bash 87 | go get github.com/gernest/resingo 88 | ``` 89 | 90 | ## Design philosophy 91 | 92 | #### Naming convention 93 | The library covers different componets of the resin service namely `device`, 94 | `application`, `os` ,`environment` and `keys`. 95 | 96 | Due to the nature of this lirary to use functions rather than attach methods to 97 | the relevant structs. We use a simple strategy of naming functions that operates 98 | for the different components. This is by adding Prefix that represent the resin 99 | component. 100 | 101 | The following is the Prefix table that shows all the prefix used to name the 102 | functions. For example `DevGetAll` is a function that retrives all devices and 103 | `AppGetAll` is a function that retrieves all App;ications. 104 | 105 | component | function prefix 106 | ------------|---------------- 107 | Device | Dev 108 | Application | App 109 | EnvironMent | Env 110 | Os | Os 111 | Keys | Key 112 | 113 | #### Functions over methods 114 | This library favors functions to provide functionality that interact with resin. 115 | All this functions accepts `Context` as the first argument, and can optionally 116 | accepts other arguments which are function specific( Note that this `Context` is 117 | defined in this libary, don't mistook it for the `context.Context`). 118 | 119 | The reason behind this is to provide composability and making the codebase 120 | clean. Also it is much easier for testing. 121 | 122 | 123 | # Usage 124 | 125 | ```go 126 | 127 | package main 128 | 129 | import ( 130 | "fmt" 131 | "log" 132 | "net/http" 133 | 134 | "github.com/gernest/resingo" 135 | ) 136 | 137 | func main() { 138 | 139 | // We need the context from which we will be talking to the resin API 140 | ctx := &resingo.Context{ 141 | Client: &http.Client{}, 142 | Config: &resingo.Config{ 143 | Username: "Your resing username", 144 | Password: "your resubn password", 145 | }, 146 | } 147 | 148 | /// There are two ways to authenticate the context ctx. 149 | 150 | // Authenticate with credentials i.e Username and password 151 | err := resingo.Login(ctx, resingo.Credentials) 152 | if err != nil { 153 | log.Fatal(err) 154 | } 155 | 156 | // Or using authentication token, which you can easily find on your resin 157 | // dashboard 158 | err = resingo.Login(ctx, resingo.AuthToken, "Tour authentication token goes here") 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | 163 | // Now the ctx is authenticated you can pass it as the first arument to any 164 | // resingo API function. 165 | // 166 | // The ctx is safe for concurrent use 167 | 168 | // Get All your applications 169 | apps, err := resingo.AppGetAll(ctx) 170 | if err != nil { 171 | log.Fatal(err) 172 | } 173 | for _, a := range apps { 174 | fmt.Println(*a) 175 | } 176 | } 177 | ``` 178 | 179 | 180 | 181 | # Contributing 182 | 183 | This requires go1.7+ 184 | 185 | Running tests will require a valid resin account . You need to set the following 186 | environment variables before running the `make` command. 187 | 188 | ```bash 189 | export RESINTEST_EMAIL= 190 | export RESINTEST_PASSWORD= 191 | export RESINTEST_USERNAME= 192 | export RESINTEST_REALDEVICE_UUID= 193 | ``` 194 | 195 | The names are self explanatory. To avoid typing them all the time, you can write 196 | them into a a file named `.env` which stays at the root of this project, the test 197 | script will automatically source it for you. 198 | 199 | All contributions are welcome. 200 | 201 | # Author 202 | 203 | twitter [@gernesti](https://twitter.com/gernesti) 204 | 205 | # Licence 206 | MIT 207 | -------------------------------------------------------------------------------- /device_test.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestDevice(t *testing.T) { 11 | config := &Config{ 12 | Username: ENV.Username, 13 | Password: ENV.Password, 14 | ResinEndpoint: apiEndpoint, 15 | } 16 | client := &http.Client{} 17 | ctx := &Context{ 18 | Client: client, 19 | Config: config, 20 | } 21 | err := Login(ctx, Credentials) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | appName := "device_test" 26 | app, err := AppCreate(ctx, appName, RaspberryPi3) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer func() { 31 | _, _ = AppDelete(ctx, app.ID) 32 | }() 33 | maxDevices := 4 34 | devices := make([]struct { 35 | uuid string 36 | dev *Device 37 | }, maxDevices) 38 | for i := 0; i < maxDevices; i++ { 39 | uid, err := GenerateUUID() 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | devices[i].uuid = uid 44 | } 45 | t.Run("Register", func(ts *testing.T) { 46 | for _, d := range devices { 47 | testDevRegister(ctx, ts, appName, d.uuid) 48 | } 49 | }) 50 | t.Run("GetByUUID", func(ts *testing.T) { 51 | for i, d := range devices { 52 | devices[i].dev = testDevGetByUUID(ctx, ts, d.uuid) 53 | } 54 | }) 55 | t.Run("GetByName", func(ts *testing.T) { 56 | for _, d := range devices { 57 | testDevGetByName(ctx, ts, d.dev.Name) 58 | } 59 | }) 60 | t.Run("GetAllByApp", func(ts *testing.T) { 61 | testDevGetAllByApp(ctx, ts, app.ID, maxDevices) 62 | }) 63 | t.Run("GetAll", func(ts *testing.T) { 64 | testDevGetAll(ctx, ts, appName, maxDevices) 65 | }) 66 | t.Run("Rename", func(ts *testing.T) { 67 | testDevRename(ctx, ts, "avocado", devices[0].uuid) 68 | }) 69 | t.Run("GetApp", func(ts *testing.T) { 70 | testDevGetApp(ctx, ts, devices[0].uuid, appName) 71 | }) 72 | t.Run("EnableURL", func(ts *testing.T) { 73 | uuid := os.Getenv("RESINTEST_REALDEVICE_UUID") 74 | if uuid == "" { 75 | ts.Skip("missing RESINTEST_REALDEVICE_UUID") 76 | } 77 | testDevEnableURL(ctx, ts, devices[0].uuid) 78 | }) 79 | t.Run("DisableURL", func(ts *testing.T) { 80 | uuid := os.Getenv("RESINTEST_REALDEVICE_UUID") 81 | if uuid == "" { 82 | ts.Skip("missing RESINTEST_REALDEVICE_UUID") 83 | } 84 | testDevDisableURL(ctx, ts, devices[0].uuid) 85 | }) 86 | t.Run("Delete", func(ts *testing.T) { 87 | u, _ := GenerateUUID() 88 | d, err := DevRegister(ctx, appName, u) 89 | if err != nil { 90 | ts.Fatal(err) 91 | } 92 | err = DevDelete(ctx, d.ID) 93 | if err != nil { 94 | ts.Fatal(err) 95 | } 96 | _, err = DevGetByUUID(ctx, u) 97 | if err != ErrDeviceNotFound { 98 | ts.Errorf("expected %s got %s", ErrDeviceNotFound.Error(), err.Error()) 99 | } 100 | }) 101 | t.Run("Note", func(ts *testing.T) { 102 | note := "hello,world" 103 | err := DevNote(ctx, devices[0].dev.ID, note) 104 | if err != nil { 105 | ts.Fatal(err) 106 | } 107 | }) 108 | env := []struct { 109 | key, value string 110 | }{ 111 | {"Mad", "Scientist"}, 112 | {"MONIKER", "IOT"}, 113 | } 114 | t.Run("CreateEnv", func(ts *testing.T) { 115 | for _, v := range devices { 116 | for _, e := range env { 117 | en, err := EnvDevCreate(ctx, v.dev.ID, e.key, e.value) 118 | if err != nil { 119 | ts.Error(err) 120 | } 121 | if en.Name != e.key { 122 | ts.Errorf("expected %s got %s", e.key, en.Name) 123 | } 124 | } 125 | } 126 | }) 127 | t.Run("EnvGetAll", func(ts *testing.T) { 128 | for _, v := range devices { 129 | envs, err := EnvDevGetAll(ctx, v.dev.ID) 130 | if err != nil { 131 | ts.Error(err) 132 | } 133 | if len(envs) != len(env) { 134 | ts.Errorf("expected %d got %d", len(env), len(envs)) 135 | } 136 | } 137 | }) 138 | t.Run("EnvUpdate", func(ts *testing.T) { 139 | envs, err := EnvDevGetAll(ctx, devices[0].dev.ID) 140 | if err != nil { 141 | ts.Fatal(err) 142 | } 143 | for _, e := range envs { 144 | err := EnvDevUpdate(ctx, e.ID, e.Name) 145 | if err != nil { 146 | ts.Error(err) 147 | } 148 | } 149 | }) 150 | t.Run("EnvDelete", func(ts *testing.T) { 151 | envs, err := EnvDevGetAll(ctx, devices[0].dev.ID) 152 | if err != nil { 153 | ts.Fatal(err) 154 | } 155 | for _, e := range envs { 156 | err := EnvDevDelete(ctx, e.ID) 157 | if err != nil { 158 | ts.Error(err) 159 | } 160 | } 161 | }) 162 | } 163 | 164 | func testDevGetAll(ctx *Context, t *testing.T, appName string, expect int) { 165 | dev, err := DevGetAll(ctx) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | if len(dev) < expect { 170 | t.Errorf("expected %d devices got %d ", expect, len(dev)) 171 | } 172 | } 173 | 174 | func testDevRegister(ctx *Context, t *testing.T, appname, uuid string) { 175 | dev, err := DevRegister(ctx, appname, uuid) 176 | if err != nil { 177 | t.Error(err) 178 | } 179 | if dev != nil { 180 | if dev.UUID != uuid { 181 | t.Error("device uuid mismatch") 182 | } 183 | } 184 | } 185 | 186 | func testDevGetByUUID(ctx *Context, t *testing.T, uuid string) *Device { 187 | dev, err := DevGetByUUID(ctx, uuid) 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | if dev.UUID != uuid { 192 | t.Fatalf("expected %s got %s", uuid, dev.UUID) 193 | } 194 | return dev 195 | } 196 | func testDevGetByName(ctx *Context, t *testing.T, name string) { 197 | dev, err := DevGetByName(ctx, name) 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | if dev.Name != name { 202 | t.Errorf("expected %s got %s", name, dev.Name) 203 | } 204 | } 205 | func testDevGetAllByApp(ctx *Context, t *testing.T, appID int64, expect int) { 206 | dev, err := DevGetAllByApp(ctx, appID) 207 | if err != nil { 208 | t.Fatal(err) 209 | } 210 | if len(dev) != expect { 211 | t.Errorf("expected %d devies got %d", expect, len(dev)) 212 | } 213 | } 214 | 215 | func testDevRename(ctx *Context, t *testing.T, newName string, uuid string) { 216 | err := DevRename(ctx, uuid, newName) 217 | if err != nil { 218 | t.Fatal(err) 219 | } 220 | dev, err := DevGetByUUID(ctx, uuid) 221 | if err != nil { 222 | t.Fatal(err) 223 | } 224 | if dev.Name != newName { 225 | t.Errorf("expected %s got %s", newName, dev.Name) 226 | } 227 | } 228 | 229 | func testDevGetApp(ctx *Context, t *testing.T, uuid, appName string) { 230 | app, err := DevGetApp(ctx, uuid) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | if app.Name != appName { 235 | t.Errorf("expected %s got %s", appName, app.Name) 236 | } 237 | 238 | } 239 | 240 | func testDevEnableURL(ctx *Context, t *testing.T, uuid string) { 241 | err := DevEnableURL(ctx, uuid) 242 | if err != nil { 243 | t.Fatal(err) 244 | } 245 | dev, err := DevGetByUUID(ctx, uuid) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | if !dev.WebAccessible { 250 | t.Error("the device should be web accessible") 251 | } 252 | } 253 | func testDevDisableURL(ctx *Context, t *testing.T, uuid string) { 254 | err := DevDisableURL(ctx, uuid) 255 | if err != nil { 256 | t.Fatal(err) 257 | } 258 | dev, err := DevGetByUUID(ctx, uuid) 259 | if err != nil { 260 | t.Fatal(err) 261 | } 262 | if dev.WebAccessible { 263 | t.Error("the device should not be web accessible") 264 | } 265 | } 266 | 267 | func TestDump(t *testing.T) { 268 | config := &Config{ 269 | Username: ENV.Username, 270 | Password: ENV.Password, 271 | ResinEndpoint: apiEndpoint, 272 | } 273 | client := &http.Client{} 274 | ctx := &Context{ 275 | Client: client, 276 | Config: config, 277 | } 278 | err := Login(ctx, Credentials) 279 | if err != nil { 280 | t.Fatal(err) 281 | } 282 | //d, _ := DevGetByUUID(ctx, "b594dafa5fd01be0a029a44e64e657d58c0f4d31652c956712b687e3f331a6") 283 | //fmt.Println(d.LogsChannel) 284 | uuid := "b594dafa5fd01be0a029a44e64e657d58c0f4d31652c956712b687e3f331a6" 285 | lg, err := NewLogs(ctx) 286 | if err != nil { 287 | t.Fatal(err) 288 | } 289 | go func() { 290 | err = lg.Log(uuid, os.Stdout) 291 | if err != nil { 292 | t.Fatal(err) 293 | } 294 | }() 295 | time.Sleep(30 * time.Second) 296 | lg.Close() 297 | 298 | } 299 | -------------------------------------------------------------------------------- /resin.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | jwt "github.com/dgrijalva/jwt-go" 16 | ) 17 | 18 | //AuthType is the authentication type that is used to authenticate with a 19 | //resin.io api. 20 | type AuthType int 21 | 22 | // supported authentication types 23 | const ( 24 | Credentials AuthType = iota 25 | AuthToken 26 | ) 27 | 28 | const ( 29 | apiEndpoint = "https://api.resin.io" 30 | ) 31 | 32 | //APIVersion is the version of resin API 33 | type APIVersion int 34 | 35 | // supported resin API versions 36 | const ( 37 | VersionOne APIVersion = iota 38 | VersionTwo 39 | VersionThree 40 | ) 41 | 42 | func (v APIVersion) String() string { 43 | switch v { 44 | case VersionOne: 45 | return "v1" 46 | case VersionTwo: 47 | return "v2" 48 | case VersionThree: 49 | return "v3" 50 | } 51 | return "" 52 | } 53 | 54 | //ErrUnkownAuthType error returned when the type of authentication is not 55 | //supported. 56 | var ErrUnkownAuthType = errors.New("resingo: unknown authentication type") 57 | 58 | //ErrMissingCredentials error returned when either username or password is 59 | //missing 60 | var ErrMissingCredentials = errors.New("resingo: missing credentials( username or password)") 61 | 62 | //ErrBadToken error returned when the resin session token is bad. 63 | var ErrBadToken = errors.New("resingo: bad session token") 64 | 65 | //HTTPClient is an interface for a http clinet that is used to communicate with 66 | //the resin API 67 | type HTTPClient interface { 68 | Do(*http.Request) (*http.Response, error) 69 | Post(url string, bodyTyp string, body io.Reader) (*http.Response, error) 70 | } 71 | 72 | //Context holds information necessary to make a call to the resin API 73 | type Context struct { 74 | Client HTTPClient 75 | Config *Config 76 | } 77 | 78 | //Config is the configuration object for the Client 79 | type Config struct { 80 | AuthToken string 81 | Username string 82 | Password string 83 | APIKey string 84 | tokenClain *TokenClain 85 | ResinEndpoint string 86 | ResinVersion APIVersion 87 | } 88 | 89 | //TokenClain are the values that are encoded into a session token from resin.io. 90 | // 91 | // It embeds jst.StandardClaims, so as to help with Verification of expired 92 | // data. Resin doens't do claim verification :(. 93 | type TokenClain struct { 94 | Username string `json:"username"` 95 | UserID int64 `json:"id"` 96 | Email string `json:"email"` 97 | jwt.StandardClaims 98 | } 99 | 100 | // formats a proper url forthe API call. The format is 101 | // /// time.Now().Unix() 130 | } 131 | 132 | //UserID returns the user id. 133 | func (c *Config) UserID() int64 { 134 | return c.tokenClain.UserID 135 | } 136 | 137 | func authHeader(token string) http.Header { 138 | h := make(http.Header) 139 | h.Add("Authorization", "Bearer "+token) 140 | return h 141 | } 142 | 143 | //SaveToken saves token, to the current Configuration object. 144 | func (c *Config) SaveToken(tok string) error { 145 | tk, err := ParseToken(tok) 146 | if err != nil { 147 | return err 148 | } 149 | c.tokenClain = tk 150 | c.AuthToken = tok 151 | return nil 152 | } 153 | 154 | //ParseToken parses the given token and extracts the claims emcode into it. This 155 | //function uses JWT method to parse the token, with verification of claims 156 | //turned off. 157 | func ParseToken(tok string) (*TokenClain, error) { 158 | p := jwt.Parser{ 159 | SkipClaimsValidation: true, 160 | } 161 | tk, _ := p.ParseWithClaims(tok, &TokenClain{}, func(token *jwt.Token) (interface{}, error) { 162 | return nil, nil 163 | }) 164 | claims, ok := tk.Claims.(*TokenClain) 165 | if ok { 166 | return claims, nil 167 | } 168 | return nil, ErrBadToken 169 | } 170 | 171 | //Authenticate authenticates the client and returns the Auth token. See Login if 172 | //you want to save the token in the client. This function does not save the 173 | //authentication token and user detals. 174 | func Authenticate(ctx *Context, typ AuthType, authToken ...string) (string, error) { 175 | loginURL := apiEndpoint + "/login_" 176 | switch typ { 177 | case Credentials: 178 | // Absence of either username or password result in missing creadentials 179 | // error. 180 | if ctx.Config.Username == "" || ctx.Config.Password == "" { 181 | return "", ErrMissingCredentials 182 | } 183 | form := url.Values{} 184 | form.Add("username", ctx.Config.Username) 185 | form.Add("password", ctx.Config.Password) 186 | res, err := ctx.Client.Post(loginURL, 187 | "application/x-www-form-urlencoded", 188 | strings.NewReader(form.Encode())) 189 | if err != nil { 190 | return "", err 191 | } 192 | defer func() { 193 | _ = res.Body.Close() 194 | }() 195 | data, err := ioutil.ReadAll(res.Body) 196 | if err != nil { 197 | return "", err 198 | } 199 | return string(data), nil 200 | case AuthToken: 201 | if len(authToken) > 0 { 202 | tk := authToken[0] 203 | if ctx.Config.IsValidToken(tk) { 204 | return tk, nil 205 | } 206 | return "", ErrBadToken 207 | } 208 | return "", errors.New("resingo: Failed to authenticate missing authToken") 209 | } 210 | return "", ErrUnkownAuthType 211 | } 212 | 213 | //Login authenticates the contextand stores the session token. This function 214 | //checks the validity of the session token before saving it. 215 | // 216 | // The call to ctx.IsLoged() should return true if the returned error is nil. 217 | func Login(ctx *Context, authTyp AuthType, authToken ...string) error { 218 | tok, err := Authenticate(ctx, authTyp, authToken...) 219 | if err != nil { 220 | return err 221 | } 222 | if ctx.Config.IsValidToken(tok) { 223 | return ctx.Config.SaveToken(tok) 224 | } 225 | return errors.New("resingo: Failed to login") 226 | } 227 | 228 | //Encode encode properly the request params for use with resin API. 229 | // 230 | // Encode tartegts the filter param, which for some reasom(based on OData) is 231 | // supposed to be $filter and not filter. The value specified by the eq param 232 | // key is combined with the value from the fileter key to produce the $filter 233 | // value string. 234 | // 235 | // Any other url params are encoded by the default encoder from 236 | // url.Values.Encoder. 237 | //TODO: check a better way to encode OData url params. 238 | func Encode(q url.Values) string { 239 | if q == nil { 240 | return "" 241 | } 242 | var buf bytes.Buffer 243 | var keys []string 244 | for k := range q { 245 | keys = append(keys, k) 246 | } 247 | for _, k := range keys { 248 | switch k { 249 | case "filter": 250 | if buf.Len() != 0 { 251 | _, _ = buf.WriteRune('&') 252 | } 253 | v := q.Get("filter") 254 | _, _ = buf.WriteString("$filter=" + v) 255 | for _, fk := range keys { 256 | switch fk { 257 | case "eq": 258 | fv := "%20" + fk + "%20" + quote(q.Get(fk)) 259 | _, _ = buf.WriteString(fv) 260 | q.Del(fk) 261 | } 262 | } 263 | q.Del(k) 264 | case "expand": 265 | if buf.Len() != 0 { 266 | _, _ = buf.WriteRune('&') 267 | } 268 | v := q.Get("expand") 269 | _, _ = buf.WriteString("$expand=" + v) 270 | q.Del(k) 271 | } 272 | } 273 | e := q.Encode() 274 | if e != "" { 275 | if buf.Len() != 0 { 276 | _, _ = buf.WriteRune('&') 277 | } 278 | _, _ = buf.WriteString(e) 279 | } 280 | return buf.String() 281 | } 282 | 283 | func quote(v string) string { 284 | ok, _ := strconv.ParseBool(v) 285 | if ok { 286 | return v 287 | } 288 | _, err := strconv.Atoi(v) 289 | if err == nil { 290 | return v 291 | } 292 | _, err = strconv.ParseFloat(v, 64) 293 | if err == nil { 294 | return v 295 | } 296 | return "'" + v + "'" 297 | } 298 | -------------------------------------------------------------------------------- /device.go: -------------------------------------------------------------------------------- 1 | package resingo 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net/url" 10 | "time" 11 | 12 | "github.com/guregu/null" 13 | ) 14 | 15 | //ErrDeviceNotFound is returned when there is no device returned from an API call 16 | //for devices. 17 | // 18 | // We assume that, a valid API call will alwayss return with valid results, so 19 | // lack of any matching devices means we didn't find anything. 20 | // 21 | // NOTE: This should have been handled by resin api. Probably with status codes 22 | // and the response body indicating nothing was dound. 23 | var ErrDeviceNotFound = errors.New("resingo: device not found") 24 | 25 | //Device represent the information about a resin device 26 | type Device struct { 27 | ID int64 `json:"id"` 28 | Name string `json:"name"` 29 | WebAccessible bool `json:"is_web_accessible"` 30 | Type string `json:"device_type"` 31 | Application struct { 32 | ID int64 `json:"__id"` 33 | Metadata struct { 34 | URI string `json:"uri"` 35 | } `json:"__deferred"` 36 | } `json:"application"` 37 | UUID string `json:"uuid"` 38 | User User `json:"user"` 39 | Actor int64 `json:"actor"` 40 | IsOnline bool `json:"is_online"` 41 | Commit string `json:"commit"` 42 | Status string `json:"status"` 43 | LastConnectivityEvent null.Time `json:"last_connectivity_event"` 44 | IP string `json:"ip_address"` 45 | VPNAddr string `json:"vpn_address"` 46 | PublicAddr string `json:"public_address"` 47 | SuprevisorVersion string `json:"supervisor_version"` 48 | Note string `json:"note"` 49 | OsVersion string `json:"os_version"` 50 | Location string `json:"location"` 51 | Longitude string `json:"longitude"` 52 | Latitude string `json:"latitude"` 53 | LogsChannel string `json:"logs_channel"` 54 | } 55 | 56 | //DevGetAll returns all devices that belong to the user who authorized the 57 | //context ctx. 58 | func DevGetAll(ctx *Context) ([]*Device, error) { 59 | h := authHeader(ctx.Config.AuthToken) 60 | uri := ctx.Config.APIEndpoint("device") 61 | b, err := doJSON(ctx, "GET", uri, h, nil, nil) 62 | if err != nil { 63 | return nil, err 64 | } 65 | var devRes = struct { 66 | D []*Device `json:"d"` 67 | }{} 68 | err = json.Unmarshal(b, &devRes) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return devRes.D, nil 73 | } 74 | 75 | //GenerateUUID generates uuid suitable for resin devices 76 | func GenerateUUID() (string, error) { 77 | src := make([]byte, 31) 78 | _, err := rand.Read(src) 79 | if err != nil { 80 | return "", err 81 | } 82 | return hex.EncodeToString(src), nil 83 | } 84 | 85 | //DevRegister registers the device with uuid to the the application with name 86 | //appName. 87 | func DevRegister(ctx *Context, appName, uuid string) (*Device, error) { 88 | app, err := AppGetByName(ctx, appName) 89 | if err != nil { 90 | return nil, err 91 | } 92 | appKey, err := AppGetAPIKey(ctx, appName) 93 | if err != nil { 94 | return nil, err 95 | } 96 | h := authHeader(ctx.Config.AuthToken) 97 | uri := ctx.Config.APIEndpoint("device") 98 | data := make(map[string]interface{}) 99 | data["user"] = ctx.Config.UserID() 100 | data["device_type"] = app.DeviceType 101 | data["application"] = app.ID 102 | data["registered_at"] = time.Now().Unix() 103 | data["uuid"] = uuid 104 | data["apikey"] = appKey 105 | body, err := marhsalReader(data) 106 | if err != nil { 107 | return nil, err 108 | } 109 | b, err := doJSON(ctx, "POST", uri, h, nil, body) 110 | //fmt.Println(string(b)) 111 | if err != nil { 112 | return nil, err 113 | } 114 | rst := &Device{} 115 | err = json.Unmarshal(b, rst) 116 | if err != nil { 117 | return nil, err 118 | } 119 | return rst, nil 120 | 121 | } 122 | 123 | //DevGetByUUID returns the device with the given uuid. 124 | func DevGetByUUID(ctx *Context, uuid string) (*Device, error) { 125 | h := authHeader(ctx.Config.AuthToken) 126 | uri := ctx.Config.APIEndpoint("device") 127 | params := make(url.Values) 128 | params.Set("filter", "uuid") 129 | params.Set("eq", uuid) 130 | b, err := doJSON(ctx, "GET", uri, h, params, nil) 131 | if err != nil { 132 | return nil, err 133 | } 134 | var devRes = struct { 135 | D []*Device `json:"d"` 136 | }{} 137 | //fmt.Println(string(b)) 138 | err = json.Unmarshal(b, &devRes) 139 | if err != nil { 140 | return nil, err 141 | } 142 | if len(devRes.D) > 0 { 143 | return devRes.D[0], nil 144 | } 145 | return nil, ErrDeviceNotFound 146 | } 147 | 148 | //DevGetByName returns the device with the given name 149 | func DevGetByName(ctx *Context, name string) (*Device, error) { 150 | h := authHeader(ctx.Config.AuthToken) 151 | uri := ctx.Config.APIEndpoint("device") 152 | params := make(url.Values) 153 | params.Set("filter", "name") 154 | params.Set("eq", name) 155 | b, err := doJSON(ctx, "GET", uri, h, params, nil) 156 | if err != nil { 157 | return nil, err 158 | } 159 | //fmt.Println(string(b)) 160 | var devRes = struct { 161 | D []*Device `json:"d"` 162 | }{} 163 | err = json.Unmarshal(b, &devRes) 164 | if err != nil { 165 | return nil, err 166 | } 167 | if len(devRes.D) > 0 { 168 | //fmt.Println(*devRes.D[0]) 169 | return devRes.D[0], nil 170 | } 171 | return nil, ErrDeviceNotFound 172 | } 173 | 174 | //DevIsOnline return true if the device with uuid is online and false otherwise. 175 | //Any errors encountered is returned too. 176 | func DevIsOnline(ctx *Context, uuid string) (bool, error) { 177 | dev, err := DevGetByUUID(ctx, uuid) 178 | if err != nil { 179 | return false, err 180 | } 181 | return dev.IsOnline, nil 182 | } 183 | 184 | //DevGetAllByApp returns all devices that are registered to the application with 185 | //the given appID 186 | func DevGetAllByApp(ctx *Context, appID int64) ([]*Device, error) { 187 | h := authHeader(ctx.Config.AuthToken) 188 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("application(%d)", appID)) 189 | params := make(url.Values) 190 | params.Set("expand", "device") 191 | b, err := doJSON(ctx, "GET", uri, h, params, nil) 192 | if err != nil { 193 | return nil, err 194 | } 195 | //var devRes = struct { 196 | //D []*Device `json:"d"` 197 | //}{} 198 | var devRes = struct { 199 | D []struct { 200 | Device []*Device `json:"device"` 201 | } `json:"d"` 202 | }{} 203 | //fmt.Println(string(b)) 204 | err = json.Unmarshal(b, &devRes) 205 | if err != nil { 206 | return nil, err 207 | } 208 | if len(devRes.D) > 0 { 209 | return devRes.D[0].Device, nil 210 | } 211 | return nil, ErrDeviceNotFound 212 | } 213 | 214 | //DevRename renames the device with uuid to nwName 215 | func DevRename(ctx *Context, uuid, newName string) error { 216 | _, err := DevGetByUUID(ctx, uuid) 217 | if err != nil { 218 | return err 219 | } 220 | h := authHeader(ctx.Config.AuthToken) 221 | uri := ctx.Config.APIEndpoint("device") 222 | params := make(url.Values) 223 | params.Set("Filter", "uuid") 224 | params.Set("eq", uuid) 225 | data := make(map[string]interface{}) 226 | data["name"] = newName 227 | body, err := marhsalReader(data) 228 | if err != nil { 229 | return err 230 | } 231 | b, err := doJSON(ctx, "PATCH", uri, h, params, body) 232 | if err != nil { 233 | return err 234 | } 235 | if string(b) != "OK" { 236 | return errors.New("bad response") 237 | } 238 | return nil 239 | } 240 | 241 | //DevGetApp returns the application in which the device belongs to. This 242 | //function is convenient only when you are interested on other information about 243 | //the application. 244 | // 245 | // If your intention is only to retrieve the applicayion id, then just use this 246 | // instead. 247 | // 248 | // dev,err:=DevGetByUUID(ctx,) 249 | // if err!=nil{ 250 | // //handle error error 251 | // } 252 | // // you can now access the application id like this 253 | // fmt.Println(dev.Application.ID 254 | func DevGetApp(ctx *Context, uuid string) (*Application, error) { 255 | dev, err := DevGetByUUID(ctx, uuid) 256 | if err != nil { 257 | return nil, err 258 | } 259 | return AppGetByID(ctx, dev.Application.ID) 260 | } 261 | 262 | //DevEnableURL enables the device url. This allows the device to be accessed 263 | //anywhere using the url which uses resin vpn. 264 | // 265 | // NOTE: It is awskward to retrurn OK rather than the url which was enabled. 266 | func DevEnableURL(ctx *Context, uuid string) error { 267 | dev, err := DevGetByUUID(ctx, uuid) 268 | if err != nil { 269 | return err 270 | } 271 | h := authHeader(ctx.Config.AuthToken) 272 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("device(%d)", dev.ID)) 273 | params := make(url.Values) 274 | params.Set("filter", "uuid") 275 | params.Set("eq", uuid) 276 | data := make(map[string]interface{}) 277 | data["is_web_accessible"] = true 278 | body, err := marhsalReader(data) 279 | if err != nil { 280 | return err 281 | } 282 | b, err := doJSON(ctx, "PATCH", uri, h, params, body) 283 | if err != nil { 284 | return err 285 | } 286 | if string(b) != "OK" { 287 | return errors.New("bad response") 288 | } 289 | return nil 290 | } 291 | 292 | //DevDisableURL diables the deice url, making it not accessible via the web. 293 | func DevDisableURL(ctx *Context, uuid string) error { 294 | dev, err := DevGetByUUID(ctx, uuid) 295 | if err != nil { 296 | return err 297 | } 298 | h := authHeader(ctx.Config.AuthToken) 299 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("device(%d)", dev.ID)) 300 | data := make(map[string]interface{}) 301 | data["is_web_accessible"] = false 302 | body, err := marhsalReader(data) 303 | if err != nil { 304 | return err 305 | } 306 | b, err := doJSON(ctx, "PATCH", uri, h, nil, body) 307 | if err != nil { 308 | return err 309 | } 310 | if string(b) != "OK" { 311 | return errors.New("bad response") 312 | } 313 | return nil 314 | } 315 | 316 | //DevDelete deletes the device with the given id 317 | func DevDelete(ctx *Context, id int64) error { 318 | h := authHeader(ctx.Config.AuthToken) 319 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("device(%d)", id)) 320 | b, err := doJSON(ctx, "DELETE", uri, h, nil, nil) 321 | if err != nil { 322 | return err 323 | } 324 | if string(b) != "OK" { 325 | return errors.New("bad response") 326 | } 327 | return nil 328 | } 329 | 330 | //DevNote add note to the device 331 | func DevNote(ctx *Context, id int64, note string) error { 332 | h := authHeader(ctx.Config.AuthToken) 333 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("device(%d)", id)) 334 | data := make(map[string]interface{}) 335 | data["note"] = note 336 | body, err := marhsalReader(data) 337 | if err != nil { 338 | return err 339 | } 340 | b, err := doJSON(ctx, "PATCH", uri, h, nil, body) 341 | if err != nil { 342 | return err 343 | } 344 | if string(b) != "OK" { 345 | return errors.New("bad response") 346 | } 347 | return nil 348 | } 349 | 350 | //DevMove moves the device to a different application 351 | func DevMove(ctx *Context, id int64, appID int64) error { 352 | h := authHeader(ctx.Config.AuthToken) 353 | uri := ctx.Config.APIEndpoint(fmt.Sprintf("device(%d)", id)) 354 | data := make(map[string]interface{}) 355 | data["application"] = appID 356 | body, err := marhsalReader(data) 357 | if err != nil { 358 | return err 359 | } 360 | b, err := doJSON(ctx, "PATCH", uri, h, nil, body) 361 | if err != nil { 362 | return err 363 | } 364 | if string(b) != "OK" { 365 | return errors.New("bad response") 366 | } 367 | return nil 368 | } 369 | 370 | // DevBlink identifies the device by blinking. 371 | func DevBlink(ctx *Context, uuid string) error { 372 | h := authHeader(ctx.Config.AuthToken) 373 | //uri := ctx.Config.APIEndpoint("blink") 374 | uri := apiEndpoint + "/" + "blink" 375 | data := make(map[string]interface{}) 376 | data["uuid"] = uuid 377 | body, err := marhsalReader(data) 378 | if err != nil { 379 | return err 380 | } 381 | b, err := doJSON(ctx, "POST", uri, h, nil, body) 382 | if err != nil { 383 | return err 384 | } 385 | if string(b) != "OK" { 386 | return errors.New("bad response") 387 | } 388 | return nil 389 | } 390 | --------------------------------------------------------------------------------