From 3e9a087a2919c055c26074b3cd4e8cc9978ae5c2 Mon Sep 17 00:00:00 2001 From: Will Ward <80708189+willpusher@users.noreply.github.com> Date: Wed, 3 May 2023 16:52:10 +0100 Subject: [PATCH] Serverless Functions (#58) * Add function management Enables operator to create/update/delete functions via pusher api. * Fix linter feedback * Bump golang.org/x/text from 0.3.7 to 0.3.8 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8) * Fix staging Enhance config options to allow use in staging clusters * Update status in README from alpha to beta Co-authored-by: Robert Oles Co-authored-by: robertoles Co-authored-by: Ahmad Samiei Co-authored-by: Ahmad Samiei Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Agata Walukiewicz Co-authored-by: Vitaliy Kalachikhin Co-authored-by: Felipe Benevides --- Makefile | 7 + README.md | 16 +- api/account.go | 8 +- api/app.go | 10 +- api/function.go | 453 +++++++++++++++++++++++ api/shared.go | 33 +- api/token.go | 10 +- commands/auth/login.go | 3 +- commands/channels/apps.go | 3 +- commands/channels/auth_server.go | 5 +- commands/channels/channel_info.go | 9 +- commands/channels/common.go | 31 ++ commands/channels/functions.go | 519 +++++++++++++++++++++++++++ commands/channels/generate_client.go | 15 +- commands/channels/generate_server.go | 24 +- commands/channels/list_channels.go | 9 +- commands/channels/subscribe.go | 9 +- commands/channels/tokens.go | 3 +- commands/channels/trigger.go | 9 +- commands/shared_flags.go | 8 + config/config.go | 2 +- go.mod | 4 +- go.sum | 16 +- main.go | 11 +- mockgen.Dockerfile | 8 + 25 files changed, 1163 insertions(+), 62 deletions(-) create mode 100644 Makefile create mode 100644 api/function.go create mode 100644 commands/channels/common.go create mode 100644 commands/channels/functions.go create mode 100644 mockgen.Dockerfile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..db40ab7 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: build-mockgen +build-mockgen: + docker build -f mockgen.Dockerfile --tag pusher_cli_mockgen . + +.PHONY: mocks +mocks: build-mockgen + docker run --rm --volume "$$(pwd):/src" pusher_cli_mockgen diff --git a/README.md b/README.md index 8603a7a..72223b4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This is a tool that allows developers access to their Pusher accounts via a command line interface. -This is an alpha release. We recommend using it for non-production apps. It may eat your laundry! We'd appreciate your feedback. +This is a beta release. We recommend using it for non-production apps unless otherwise advised. We'd appreciate your feedback. ## Usage @@ -41,3 +41,17 @@ We [publish binaries on GitHub](https://github.com/pusher/cli/releases) and we u 1. `git tag -a v0.14 -m "v0.14"` 1. `git push origin v0.14` + +### Configuration + +`pusher login` creates a file `~/.config/pusher.json` (or updates it if it already exists). +If you need to point the Pusher CLI to different servers (e.g. when testing), you can change the `endpoint` value and add new name/value pairs as necessary: +```JSON +{ + "endpoint": "https://cli.another.domain.com", + "token": "my-secret-api-key", + "apihost": "api-mycluster.another.domain.com", + "httphost": "sockjs-mycluster.another.domain.com", + "wshost": "ws-mycluster.another.domain.com" +} +``` diff --git a/api/account.go b/api/account.go index 264df67..18d3d39 100644 --- a/api/account.go +++ b/api/account.go @@ -8,9 +8,9 @@ import ( ) //isAPIKeyValid returns true if the stored API key is valid. -func isAPIKeyValid() bool { +func (p *PusherApi) isAPIKeyValid() bool { if viper.GetString("token") != "" { - _, err := GetAllApps() + _, err := p.GetAllApps() if err == nil { return true } @@ -18,8 +18,8 @@ func isAPIKeyValid() bool { return false } -func validateKeyOrDie() { - if !isAPIKeyValid() { +func (p *PusherApi) validateKeyOrDie() { + if !p.isAPIKeyValid() { fmt.Println("Your API key isn't valid. Add one with the `login` command.") os.Exit(1) } diff --git a/api/app.go b/api/app.go index 5d3f4cb..3c421b7 100644 --- a/api/app.go +++ b/api/app.go @@ -12,10 +12,10 @@ type App struct { Cluster string `json:"cluster"` } -const getAppsAPIEndpoint = "/apps.json" +const GetAppsAPIEndpoint = "/apps.json" -func GetAllApps() ([]App, error) { - response, err := makeRequest("GET", getAppsAPIEndpoint, nil) +func (p *PusherApi) GetAllApps() ([]App, error) { + response, err := p.makeRequest("GET", GetAppsAPIEndpoint, nil) if err != nil { return nil, err } @@ -29,8 +29,8 @@ func GetAllApps() ([]App, error) { return apps, nil } -func GetApp(appID string) (*App, error) { - apps, err := GetAllApps() +func (p *PusherApi) GetApp(appID string) (*App, error) { + apps, err := p.GetAllApps() if err != nil { return nil, err } diff --git a/api/function.go b/api/function.go new file mode 100644 index 0000000..c683e40 --- /dev/null +++ b/api/function.go @@ -0,0 +1,453 @@ +//go:generate mockgen -source function.go -destination mock/function_mock.go -package mock + +package api + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" +) + +type FunctionService interface { + GetAllFunctionsForApp(name string) ([]Function, error) + CreateFunction(appID string, name string, events []string, body io.Reader, mode string) (Function, error) + DeleteFunction(appID string, functionID string) error + GetFunction(appID string, functionID string) (Function, error) + UpdateFunction(appID string, functionID string, name string, events []string, body io.Reader, mode string) (Function, error) + GetFunctionLogs(appID string, functionID string) (FunctionLogs, error) + GetFunctionConfigsForApp(appID string) ([]FunctionConfig, error) + CreateFunctionConfig(appID string, name string, description string, paramType string, content string) (FunctionConfig, error) + UpdateFunctionConfig(appID string, name string, description string, content string) (FunctionConfig, error) + DeleteFunctionConfig(appID string, name string) error + InvokeFunction(appID string, functionId string, data string, event string, channel string) (string, error) +} + +type Function struct { + ID string `json:"id"` + Name string `json:"name"` + Events []string `json:"events"` + Mode string `json:"mode"` + Body []byte `json:"body"` +} + +type FunctionConfig struct { + Name string `json:"name"` + Description string `json:"description"` + ParamType string `json:"param_type"` +} + +type FunctionRequestBody struct { + Function FunctionRequestBodyFunction `json:"function"` +} + +type FunctionRequestBodyFunction struct { + Name string `json:"name,omitempty"` + Events *[]string `json:"events,omitempty"` + Body []byte `json:"body,omitempty"` + Mode string `json:"mode,omitempty"` +} + +type InvokeFunctionRequest struct { + Data string `json:"data"` + Event string `json:"event"` + Channel string `json:"channel"` +} + +type CreateFunctionConfigRequest struct { + Name string `json:"name"` + Description string `json:"description"` + ParamType string `json:"param_type"` + Content string `json:"content"` +} + +type UpdateFunctionConfigRequest struct { + Description string `json:"description"` + Content string `json:"content"` +} + +type FunctionLogs struct { + Events []LogEvent `json:"events"` +} + +type LogEvent struct { + Message string `json:"message"` + Timestamp int64 `json:"timestamp"` +} + +const FunctionsApiEndpoint = "/apps/%s/functions.json" +const FunctionApiEndpoint = "/apps/%s/functions/%s.json" +const FunctionConfigsApiEndpoint = "/apps/%s/function_configs.json" +const FunctionConfigApiEndpoint = "/apps/%s/function_configs/%s.json" + +var internalErr = errors.New("Pusher encountered an error, please retry") +var unauthorisedErr = errors.New("that app ID wasn't recognised as linked to your account") + +func NewCreateFunctionRequestBody(name string, events *[]string, body []byte, mode string) FunctionRequestBody { + return FunctionRequestBody{ + Function: FunctionRequestBodyFunction{ + Name: name, + Events: events, + Body: body, + Mode: mode, + }, + } +} + +func NewUpdateFunctionRequestBody(name string, events *[]string, mode string, body []byte) FunctionRequestBody { + return FunctionRequestBody{ + Function: FunctionRequestBodyFunction{ + Name: name, + Events: events, + Body: body, + Mode: mode, + }, + } +} + +func NewCreateFunctionConfigRequest(name string, description string, paramType string, content string) CreateFunctionConfigRequest { + return CreateFunctionConfigRequest{ + Name: name, + Description: description, + ParamType: paramType, + Content: content, + } +} + +func NewUpdateFunctionConfigRequest(description string, content string) UpdateFunctionConfigRequest { + return UpdateFunctionConfigRequest{ + Description: description, + Content: content, + } +} + +func (p *PusherApi) GetAllFunctionsForApp(appID string) ([]Function, error) { + response, err := p.makeRequest("GET", fmt.Sprintf(FunctionsApiEndpoint, appID), nil) + + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusForbidden: + return nil, errors.New(response) + default: + return nil, unauthorisedErr + } + default: + return nil, internalErr + } + } + + functions := []Function{} + err = json.Unmarshal([]byte(response), &functions) + if err != nil { + return nil, errors.New("Response from Pusher API was not valid json, please retry") + } + return functions, nil +} + +func (p *PusherApi) CreateFunction(appID string, name string, events []string, body io.Reader, mode string) (Function, error) { + bodyBytes, err := io.ReadAll(body) + if err != nil { + return Function{}, fmt.Errorf("could not create function archive: %w", err) + } + + var pEvents *[]string = nil + if events != nil { + pEvents = &events + } + + request := NewCreateFunctionRequestBody(name, pEvents, bodyBytes, mode) + + requestJson, err := json.Marshal(&request) + if err != nil { + return Function{}, fmt.Errorf("could not serialize function: %w", err) + } + + response, err := p.makeRequest("POST", fmt.Sprintf(FunctionsApiEndpoint, appID), requestJson) + + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return Function{}, unauthorisedErr + case http.StatusNotFound: + return Function{}, errors.New("App not found") + case http.StatusForbidden, http.StatusUnprocessableEntity: + return Function{}, errors.New(response) + default: + return Function{}, internalErr + } + default: + + return Function{}, internalErr + } + } + + function := Function{} + err = json.Unmarshal([]byte(response), &function) + if err != nil { + return Function{}, errors.New("Response from Pusher API was not valid json, please retry") + } + return function, nil +} + +func (p *PusherApi) DeleteFunction(appID string, functionID string) error { + response, err := p.makeRequest("DELETE", fmt.Sprintf(FunctionApiEndpoint, appID, functionID), nil) + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return unauthorisedErr + case http.StatusForbidden: + return errors.New(response) + case http.StatusNotFound: + return fmt.Errorf("Function with id: %s, could not be found", functionID) + default: + return internalErr + } + default: + return internalErr + } + } + + return nil +} + +func (p *PusherApi) GetFunction(appID string, functionID string) (Function, error) { + response, err := p.makeRequest("GET", fmt.Sprintf(FunctionApiEndpoint, appID, functionID), nil) + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return Function{}, unauthorisedErr + case http.StatusForbidden: + return Function{}, errors.New(response) + case http.StatusNotFound: + return Function{}, errors.New("Function could not be found") + default: + return Function{}, internalErr + } + + default: + return Function{}, internalErr + } + } + + function := Function{} + err = json.Unmarshal([]byte(response), &function) + if err != nil { + return Function{}, errors.New("Response from Pusher API was not valid json, please retry") + } + return function, nil +} + +func (p *PusherApi) UpdateFunction( + appID string, functionID string, name string, events []string, body io.Reader, mode string) (Function, error) { + var bodyBytes []byte = nil + var err error + + if body != nil { + bodyBytes, err = io.ReadAll(body) + if err != nil { + return Function{}, fmt.Errorf("could not create function archive: %w", err) + } + } + + var pEvents *[]string = nil + if events != nil { + pEvents = &events + } + request := NewUpdateFunctionRequestBody(name, pEvents, mode, bodyBytes) + + requestJson, err := json.Marshal(&request) + if err != nil { + return Function{}, fmt.Errorf("could not serialize function: %w", err) + } + response, err := p.makeRequest("PUT", fmt.Sprintf(FunctionApiEndpoint, appID, functionID), requestJson) + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return Function{}, unauthorisedErr + case http.StatusForbidden, http.StatusUnprocessableEntity: + return Function{}, errors.New(response) + default: + return Function{}, internalErr + } + default: + return Function{}, internalErr + } + } + + function := Function{} + err = json.Unmarshal([]byte(response), &function) + if err != nil { + return Function{}, errors.New("Response from Pusher API was not valid json, please retry") + } + return function, nil +} + +func (p *PusherApi) InvokeFunction(appID string, functionID string, data string, event string, channel string) (string, error) { + request := InvokeFunctionRequest{Data: data, Event: event, Channel: channel} + + requestJson, err := json.Marshal(&request) + if err != nil { + return "", fmt.Errorf("could not serialize function: %w", err) + } + response, err := p.makeRequest("POST", fmt.Sprintf("/apps/%s/functions/%s/invoke.json", appID, functionID), requestJson) + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return "", unauthorisedErr + case http.StatusForbidden: + return "", errors.New(response) + case http.StatusNotFound: + return "", errors.New("Function could not be found") + default: + return "", internalErr + } + default: + return "", internalErr + } + } + + return response, nil +} + +func (p *PusherApi) GetFunctionLogs(appID string, functionID string) (FunctionLogs, error) { + response, err := p.makeRequest("GET", fmt.Sprintf("/apps/%s/functions/%s/logs.json", appID, functionID), nil) + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return FunctionLogs{}, unauthorisedErr + case http.StatusForbidden: + return FunctionLogs{}, errors.New(response) + case http.StatusNotFound: + return FunctionLogs{}, errors.New("Function could not be found") + default: + return FunctionLogs{}, internalErr + } + default: + return FunctionLogs{}, internalErr + } + } + + logs := FunctionLogs{} + err = json.Unmarshal([]byte(response), &logs) + if err != nil { + return FunctionLogs{}, errors.New("Response from Pusher API was not valid json, please retry") + } + + return logs, nil +} + +func (p *PusherApi) GetFunctionConfigsForApp(appID string) ([]FunctionConfig, error) { + response, err := p.makeRequest("GET", fmt.Sprintf(FunctionConfigsApiEndpoint, appID), nil) + if err != nil { + return nil, errors.New("that app ID wasn't recognised as linked to your account") + } + configs := []FunctionConfig{} + err = json.Unmarshal([]byte(response), &configs) + if err != nil { + return nil, errors.New("Response from Pusher API was not valid json, please retry") + } + return configs, nil +} + +func (p *PusherApi) CreateFunctionConfig(appID string, name string, description string, paramType string, content string) (FunctionConfig, error) { + request := NewCreateFunctionConfigRequest(name, description, paramType, content) + + requestJson, err := json.Marshal(&request) + if err != nil { + return FunctionConfig{}, errors.New("Could not create function config") + } + response, err := p.makeRequest("POST", fmt.Sprintf(FunctionConfigsApiEndpoint, appID), requestJson) + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return FunctionConfig{}, unauthorisedErr + case http.StatusForbidden, http.StatusUnprocessableEntity: + return FunctionConfig{}, errors.New(response) + default: + return FunctionConfig{}, internalErr + } + default: + return FunctionConfig{}, internalErr + } + } + + functionConfig := FunctionConfig{} + err = json.Unmarshal([]byte(response), &functionConfig) + if err != nil { + return FunctionConfig{}, errors.New("Response from Pusher API was not valid json, please retry") + } + return functionConfig, nil +} + +func (p *PusherApi) UpdateFunctionConfig(appID string, name string, description string, content string) (FunctionConfig, error) { + request := NewUpdateFunctionConfigRequest(description, content) + + requestJson, err := json.Marshal(&request) + if err != nil { + return FunctionConfig{}, errors.New("Could not update function config") + } + response, err := p.makeRequest("PUT", fmt.Sprintf(FunctionConfigApiEndpoint, appID, name), requestJson) + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return FunctionConfig{}, unauthorisedErr + case http.StatusForbidden, http.StatusUnprocessableEntity: + return FunctionConfig{}, errors.New(response) + default: + return FunctionConfig{}, internalErr + } + default: + return FunctionConfig{}, internalErr + } + } + + functionConfig := FunctionConfig{} + err = json.Unmarshal([]byte(response), &functionConfig) + if err != nil { + return FunctionConfig{}, errors.New("Response from Pusher API was not valid json, please retry") + } + return functionConfig, nil +} + +func (p *PusherApi) DeleteFunctionConfig(appID string, name string) error { + response, err := p.makeRequest("DELETE", fmt.Sprintf(FunctionConfigApiEndpoint, appID, name), nil) + + if err != nil { + switch e := err.(type) { + case *HttpStatusError: + switch e.StatusCode { + case http.StatusUnauthorized: + return unauthorisedErr + case http.StatusForbidden, http.StatusUnprocessableEntity: + return errors.New(response) + case http.StatusNotFound: + return fmt.Errorf("Function config with name: %s, could not be found", name) + default: + return internalErr + } + default: + return internalErr + } + } + + return nil +} diff --git a/api/shared.go b/api/shared.go index 1883b3c..b101ab2 100644 --- a/api/shared.go +++ b/api/shared.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "fmt" "io/ioutil" "net/http" "time" @@ -11,17 +12,37 @@ import ( ) var ( - httpClient = &http.Client{Timeout: 5 * time.Second} + httpClient = &http.Client{Timeout: 10 * time.Second} ) -func makeRequest(reqtype string, path string, body []byte) (string, error) { - req, err := http.NewRequest(reqtype, viper.GetString("endpoint")+path, bytes.NewBuffer(body)) +type HttpStatusError struct { + StatusCode int +} + +func (e *HttpStatusError) Error() string { + return fmt.Sprintf("http status code: %d", e.StatusCode) +} + +type PusherApi struct { +} + +func NewPusherApi() *PusherApi { + return &PusherApi{} +} + +func (p *PusherApi) requestUrl(path string) string { + return viper.GetString("endpoint") + path +} + +func (p *PusherApi) makeRequest(reqtype string, path string, body []byte) (string, error) { + req, err := http.NewRequest(reqtype, p.requestUrl(path), bytes.NewBuffer(body)) if err != nil { return "", err } req.Header.Set("Content-type", "application/json") req.Header.Set("Authorization", "Token token="+viper.GetString("token")) req.Header.Set("User-Agent", "PusherCLI/"+config.GetVersion()) + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return "", err @@ -34,5 +55,11 @@ func makeRequest(reqtype string, path string, body []byte) (string, error) { return "", err } + if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) { + return string(respBody), &HttpStatusError{ + StatusCode: resp.StatusCode, + } + } + return string(respBody), nil } diff --git a/api/token.go b/api/token.go index e8133c8..4f34f43 100644 --- a/api/token.go +++ b/api/token.go @@ -13,9 +13,9 @@ type AppToken struct { const getTokensAPIEndpoint = "/apps/%s/tokens.json" // Interpolate with `appId` -func GetAllTokensForApp(appId string) ([]AppToken, error) { - validateKeyOrDie() - response, err := makeRequest("GET", fmt.Sprintf(getTokensAPIEndpoint, appId), nil) +func (p *PusherApi) GetAllTokensForApp(appId string) ([]AppToken, error) { + p.validateKeyOrDie() + response, err := p.makeRequest("GET", fmt.Sprintf(getTokensAPIEndpoint, appId), nil) if err != nil { return nil, err } @@ -27,8 +27,8 @@ func GetAllTokensForApp(appId string) ([]AppToken, error) { return tokens, nil } -func GetToken(appId string) (*AppToken, error) { - tokens, err := GetAllTokensForApp(appId) +func (p *PusherApi) GetToken(appId string) (*AppToken, error) { + tokens, err := p.GetAllTokensForApp(appId) if err != nil { return nil, err } diff --git a/commands/auth/login.go b/commands/auth/login.go index d0caddd..740b3f4 100644 --- a/commands/auth/login.go +++ b/commands/auth/login.go @@ -39,8 +39,9 @@ var Login = &cobra.Command{ //APIKeyValid returns true if the stored API key is valid. func APIKeyValid() bool { + p := api.NewPusherApi() if viper.GetString("token") != "" { - _, err := api.GetAllApps() + _, err := p.GetAllApps() if err == nil { return true } diff --git a/commands/channels/apps.go b/commands/channels/apps.go index 5a4e5cc..5e13fc4 100644 --- a/commands/channels/apps.go +++ b/commands/channels/apps.go @@ -25,7 +25,8 @@ var Apps = &cobra.Command{ return } - apps, err := api.GetAllApps() + p := api.NewPusherApi() + apps, err := p.GetAllApps() if err != nil { fmt.Println("Failed to retrieve the list of apps.") os.Exit(1) diff --git a/commands/channels/auth_server.go b/commands/channels/auth_server.go index 3b94e87..c7e9e9b 100644 --- a/commands/channels/auth_server.go +++ b/commands/channels/auth_server.go @@ -34,14 +34,15 @@ var LocalAuthServer = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) diff --git a/commands/channels/channel_info.go b/commands/channels/channel_info.go index 3390379..261f44b 100644 --- a/commands/channels/channel_info.go +++ b/commands/channels/channel_info.go @@ -9,6 +9,7 @@ import ( "github.com/pusher/cli/commands" "github.com/pusher/pusher-http-go" "github.com/spf13/cobra" + "github.com/theherk/viper" ) var ChannelInfo = &cobra.Command{ @@ -29,14 +30,15 @@ var ChannelInfo = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { - fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) @@ -48,6 +50,7 @@ var ChannelInfo = &cobra.Command{ Key: token.Key, Secret: token.Secret, Cluster: app.Cluster, + Host: viper.GetString("apihost"), } infoValues := []string{} diff --git a/commands/channels/common.go b/commands/channels/common.go new file mode 100644 index 0000000..2bf74a5 --- /dev/null +++ b/commands/channels/common.go @@ -0,0 +1,31 @@ +package channels + +import ( + "fmt" + + "github.com/theherk/viper" +) + +func wsHost(cluster string) string { + host := viper.GetString("wshost") + if host == "" { + host = fmt.Sprintf("ws-%s.pusher.com", cluster) + } + return host +} + +func httpHost(cluster string) string { + host := viper.GetString("httphost") + if host == "" { + host = fmt.Sprintf("sockjs-%s.pusher.com", cluster) + } + return host +} + +func apiHost(cluster string) string { + host := viper.GetString("apihost") + if host == "" { + host = fmt.Sprintf("api-%s.pusher.com", cluster) + } + return host +} diff --git a/commands/channels/functions.go b/commands/channels/functions.go new file mode 100644 index 0000000..2ac1244 --- /dev/null +++ b/commands/channels/functions.go @@ -0,0 +1,519 @@ +package channels + +import ( + "archive/zip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "encoding/json" + + "github.com/olekukonko/tablewriter" + "github.com/pusher/cli/api" + "github.com/pusher/cli/commands" + "github.com/spf13/cobra" +) + +func NewConfigListCommand(pusher api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List function configs for an Channels app", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + configs, err := pusher.GetFunctionConfigsForApp(commands.AppID) + if err != nil { + return err + } + + if commands.OutputAsJSON { + configsJSONBytes, _ := json.Marshal(configs) + cmd.Println(string(configsJSONBytes)) + } else { + table := newTable(cmd.OutOrStdout()) + table.SetHeader([]string{"Name", "Desciption", "Type"}) + for _, config := range configs { + table.Append([]string{config.Name, config.Description, config.ParamType}) + } + table.Render() + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + return cmd +} + +func NewConfigCreateCommand(functionService api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a function config for a Channels app", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + config, err := functionService.CreateFunctionConfig(commands.AppID, commands.FunctionConfigName, commands.FunctionConfigDescription, commands.FunctionConfigParamType, commands.FunctionConfigContent) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionJSONBytes, _ := json.Marshal(config) + fmt.Fprintln(cmd.OutOrStdout(), string(functionJSONBytes)) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "created function config %s\n", config.Name) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + cmd.PersistentFlags().StringVar(&commands.FunctionConfigName, "name", "", "Function config name. Can only contain A-Za-z0-9-_") + err := cmd.MarkPersistentFlagRequired("name") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.FunctionConfigDescription, "description", "", "Function config description") + err = cmd.MarkPersistentFlagRequired("description") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.FunctionConfigParamType, "type", "", "Function config type, valid options: param|secret") + err = cmd.MarkPersistentFlagRequired("type") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.FunctionConfigContent, "content", "", "Function config contents") + err = cmd.MarkPersistentFlagRequired("content") + if err != nil { + return nil, err + } + return cmd, nil +} + +func NewConfigUpdateCommand(functionService api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "update", + Short: "Update a function config for a Channels app", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + config, err := functionService.UpdateFunctionConfig(commands.AppID, commands.FunctionConfigName, commands.FunctionConfigDescription, commands.FunctionConfigContent) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionJSONBytes, _ := json.Marshal(config) + fmt.Fprintln(cmd.OutOrStdout(), string(functionJSONBytes)) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "updated function config %s\n", config.Name) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + cmd.PersistentFlags().StringVar(&commands.FunctionConfigName, "name", "", "Function config name. Can only contain A-Za-z0-9-_") + err := cmd.MarkPersistentFlagRequired("name") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.FunctionConfigDescription, "description", "", "Function config description") + err = cmd.MarkPersistentFlagRequired("description") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.FunctionConfigContent, "content", "", "Function config contents") + err = cmd.MarkPersistentFlagRequired("content") + if err != nil { + return nil, err + } + return cmd, nil +} + +func NewConfigDeleteCommand(functionService api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a function config from a Channels app", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + err := functionService.DeleteFunctionConfig(commands.AppID, commands.FunctionConfigName) + if err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "deleted function config %s\n", commands.FunctionConfigName) + return nil + }, + } + cmd.PersistentFlags().StringVar(&commands.FunctionConfigName, "name", "", "Function config name. Can only contain A-Za-z0-9-_") + err := cmd.MarkPersistentFlagRequired("name") + if err != nil { + return nil, err + } + return cmd, nil +} + +func NewConfigCommand(pusher api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "configs", + Short: "Manage function config params for a Channels app", + Args: cobra.NoArgs, + } + cmd.AddCommand(NewConfigListCommand(pusher)) + c, err := NewConfigCreateCommand(pusher) + if err != nil { + return nil, err + } + cmd.AddCommand(c) + c, err = NewConfigUpdateCommand(pusher) + if err != nil { + return nil, err + } + cmd.AddCommand(c) + c, err = NewConfigDeleteCommand(pusher) + if err != nil { + return nil, err + } + cmd.AddCommand(c) + return cmd, nil +} + +func NewFunctionsCommand(pusher api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "functions", + Short: "Manage functions for a Channels app", + Args: cobra.NoArgs, + } + cmd.PersistentFlags().StringVar(&commands.AppID, "app-id", "", "Channels App ID") + err := cmd.MarkPersistentFlagRequired("app-id") + if err != nil { + return nil, err + } + cmd.AddCommand(NewFunctionsListCommand(pusher)) + + c, err := NewFunctionsCreateCommand(pusher) + if err != nil { + return nil, err + } + cmd.AddCommand(c) + cmd.AddCommand(NewFunctionDeleteCommand(pusher)) + cmd.AddCommand(NewFunctionGetCommand(pusher)) + c, err = NewFunctionsUpdateCommand(pusher) + if err != nil { + return nil, err + } + cmd.AddCommand(c) + cmd.AddCommand(NewFunctionGetLogsCommand(pusher)) + c, err = NewFunctionInvokeCommand(pusher) + if err != nil { + return nil, err + } + cmd.AddCommand(c) + c, err = NewConfigCommand(pusher) + if err != nil { + return nil, err + } + cmd.AddCommand(c) + return cmd, nil +} + +var Functions = &cobra.Command{ + Use: "functions", + Short: "Manage functions for a Channels app", + Args: cobra.NoArgs, +} + +func newTable(out io.Writer) *tablewriter.Table { + table := tablewriter.NewWriter(out) + table.SetBorder(false) + table.SetRowLine(false) + table.SetHeaderLine(false) + table.SetColumnSeparator("") + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + return table +} + +func NewFunctionsListCommand(functionService api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List functions for an Channels app", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + functions, err := functionService.GetAllFunctionsForApp(commands.AppID) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionsJSONBytes, _ := json.Marshal(functions) + cmd.Println(string(functionsJSONBytes)) + } else { + table := newTable(cmd.OutOrStdout()) + table.SetHeader([]string{"ID", "Name", "Mode", "Events"}) + for _, function := range functions { + table.Append([]string{function.ID, function.Name, function.Mode, strings.Join(function.Events, ",")}) + } + table.Render() + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + return cmd +} + +func cleanMode(m string) string { + switch strings.ToLower(m) { + case "sync", "synch", "synchronous": + return "synchronous" + case "async", "asynch", "asynchronous": + return "asynchronous" + default: + return m + } +} + +func NewFunctionsCreateCommand(functionService api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a function for a Channels app", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + archive := ZipFolder(args[0]) + + mode := cleanMode(commands.FunctionMode) + if mode == "" { + mode = "asynchronous" + } + + function, err := functionService.CreateFunction(commands.AppID, commands.FunctionName, commands.FunctionEvents, archive, mode) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionJSONBytes, _ := json.Marshal(function) + fmt.Fprintln(cmd.OutOrStdout(), string(functionJSONBytes)) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "created function %s with id: %v\n", function.Name, function.ID) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + cmd.PersistentFlags().StringVar(&commands.FunctionName, "name", "", "Function name") + err := cmd.MarkPersistentFlagRequired("name") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.FunctionMode, "mode", "asynchronous", "Function mode. Either synchronous or asynchronous") + cmd.PersistentFlags().StringSliceVar(&commands.FunctionEvents, "events", nil, "Channel events that trigger this function") + return cmd, err +} + +func NewFunctionDeleteCommand(functionService api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a function from a Channels app", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + err := functionService.DeleteFunction(commands.AppID, args[0]) + if err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "deleted function %s\n", args[0]) + return nil + }, + } + return cmd +} + +func NewFunctionInvokeCommand(functionService api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "invoke ", + Short: "invoke a function to test it", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + result, err := functionService.InvokeFunction(commands.AppID, args[0], commands.Data, commands.EventName, commands.ChannelName) + if err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "%v\n", result) + return nil + }, + } + cmd.PersistentFlags().StringVar(&commands.Data, "data", "", "Channels event data") + err := cmd.MarkPersistentFlagRequired("data") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.EventName, "event", "", "Channels event name") + err = cmd.MarkPersistentFlagRequired("event") + if err != nil { + return nil, err + } + cmd.PersistentFlags().StringVar(&commands.ChannelName, "channel", "", "Channels name") + err = cmd.MarkPersistentFlagRequired("channel") + if err != nil { + return nil, err + } + return cmd, nil +} + +func NewFunctionGetCommand(functionService api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "Downloads a function from a Channels app", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fn, err := functionService.GetFunction(commands.AppID, args[0]) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionsJSONBytes, _ := json.Marshal(fn) + cmd.Println(string(functionsJSONBytes)) + } else { + zipFileName := fmt.Sprintf("%s.%s.zip", fn.Name, time.Now().Format("2006-01-02-150405")) + fmt.Fprintf(cmd.OutOrStdout(), "ID: %v\n", fn.ID) + fmt.Fprintf(cmd.OutOrStdout(), "Name: %v\n", fn.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Mode: %v\n", fn.Mode) + fmt.Fprintf(cmd.OutOrStdout(), "Events: %v\n", strings.Join(fn.Events, ",")) + err = os.WriteFile(zipFileName, fn.Body, 0644) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Body: '%v'\n", zipFileName) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + return cmd +} + +func NewFunctionsUpdateCommand(functionService api.FunctionService) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "update []", + Short: "Update a function for a Channels app", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + var archive io.Reader = nil + if len(args) >= 2 { + archive = ZipFolder(args[1]) + } + + function, err := functionService.UpdateFunction(commands.AppID, args[0], commands.FunctionName, commands.FunctionEvents, archive, cleanMode(commands.FunctionMode)) + if err != nil { + return err + } + + if commands.OutputAsJSON { + functionJSONBytes, _ := json.Marshal(function) + fmt.Fprintln(cmd.OutOrStdout(), string(functionJSONBytes)) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "updated function: %v\n", function.ID) + } + return nil + }, + } + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + cmd.PersistentFlags().StringVar(&commands.FunctionName, "name", "", "Function name") + cmd.PersistentFlags().StringSliceVar(&commands.FunctionEvents, "events", nil, "Channel events that trigger this function") + cmd.PersistentFlags().StringVar(&commands.FunctionMode, "mode", "", "Function mode. Either synchronous or asynchronous") + return cmd, nil +} + +func NewFunctionGetLogsCommand(functionService api.FunctionService) *cobra.Command { + cmd := &cobra.Command{ + Use: "logs ", + Short: "Get logs of a specific function", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + logs, err := functionService.GetFunctionLogs(commands.AppID, args[0]) + if err != nil { + return err + } + + if commands.OutputAsJSON { + JSONBytes, _ := json.Marshal(logs) + cmd.Println(string(JSONBytes)) + + return nil + } + + for _, l := range logs.Events { + t := time.Unix(0, l.Timestamp*1000000).Format("2006-01-02 15:04:05") + cmd.Printf("%s\t%s\n", t, l.Message) + } + + return nil + }, + } + + cmd.PersistentFlags().BoolVar(&commands.OutputAsJSON, "json", false, "") + + return cmd +} + +func ZipFolder(baseFolder string) io.Reader { + r, w := io.Pipe() + + go func() { + // Create a new zip archive. + zw := zip.NewWriter(w) + + // Recursively add files to the archive. + err := addFiles(zw, baseFolder, "") + + // Close the archive and pipeline, reporting any errors. + if err != nil { + zw.Close() + } else { + err = zw.Close() + } + w.CloseWithError(err) + }() + + return r +} + +func addFiles(w *zip.Writer, basePath, baseInZip string) error { + // Open the Directory. + files, err := ioutil.ReadDir(basePath) + if err != nil { + return err + } + + for _, file := range files { + if !file.IsDir() { + dat, err := ioutil.ReadFile(filepath.Join(basePath, file.Name())) + if err != nil { + return err + } + + // Add some files to the archive. + f, err := w.Create(filepath.Join(baseInZip, file.Name())) + if err != nil { + return err + } + _, err = f.Write(dat) + if err != nil { + return err + } + } else if file.IsDir() { + + // Recurse. + newBase := filepath.Join(basePath, file.Name()) + err = addFiles(w, newBase, filepath.Join(baseInZip, file.Name())) + if err != nil { + return err + } + } + } + return nil +} diff --git a/commands/channels/generate_client.go b/commands/channels/generate_client.go index 1a3c08c..81e1cdd 100644 --- a/commands/channels/generate_client.go +++ b/commands/channels/generate_client.go @@ -10,14 +10,14 @@ import ( "github.com/spf13/cobra" ) -//GenerateClient generates a client that can subscribe to channels on an app. +// GenerateClient generates a client that can subscribe to channels on an app. var GenerateClient = &cobra.Command{ Use: "client", Short: "Generate a client for your Channels app", Args: cobra.NoArgs, } -//GenerateWeb generates a web client that can subscribe to channels on an app. +// GenerateWeb generates a web client that can subscribe to channels on an app. var GenerateWeb = &cobra.Command{ Use: "web", Short: "Generate a web client for your Channels app", @@ -29,14 +29,15 @@ var GenerateWeb = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { - fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) return @@ -52,8 +53,8 @@ var GenerateWeb = &cobra.Command{ Pusher.logToConsole = true; var pusher = new Pusher('` + token.Key + `', { - wsHost: 'ws-` + app.Cluster + `.pusher.com', - httpHost: 'sockjs-` + app.Cluster + `.pusher.com', + wsHost: '` + wsHost(app.Cluster) + `', + httpHost: '` + httpHost(app.Cluster) + `', encrypted: true }); diff --git a/commands/channels/generate_server.go b/commands/channels/generate_server.go index 8142a9a..6aba1bd 100644 --- a/commands/channels/generate_server.go +++ b/commands/channels/generate_server.go @@ -10,14 +10,14 @@ import ( "github.com/spf13/cobra" ) -//GenerateServer generates a server app that can trigger events on a particular Pusher app. +// GenerateServer generates a server app that can trigger events on a particular Pusher app. var GenerateServer = &cobra.Command{ Use: "server", Short: "Generate a server for your Channels app", Args: cobra.NoArgs, } -//GeneratePhp generates a PHP app that can trigger events on a particular Pusher app. +// GeneratePhp generates a PHP app that can trigger events on a particular Pusher app. var GeneratePhp = &cobra.Command{ Use: "php", Short: "Generate a PHP server for your Channels app", @@ -29,14 +29,15 @@ var GeneratePhp = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { - fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) return @@ -47,7 +48,7 @@ var GeneratePhp = &cobra.Command{ require __DIR__ . '/vendor/autoload.php'; $options = array( - 'host' => 'api-` + app.Cluster + `.pusher.com', + 'host' => '` + apiHost(app.Cluster) + `', 'encrypted' => true ); @@ -66,7 +67,7 @@ var GeneratePhp = &cobra.Command{ }, } -//GeneratePython generates a Python app that can trigger events on a particular Pusher app. +// GeneratePython generates a Python app that can trigger events on a particular Pusher app. var GeneratePython = &cobra.Command{ Use: "python", Short: "Generate a Python server for your Channels app", @@ -78,14 +79,15 @@ var GeneratePython = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { - fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) return @@ -96,7 +98,7 @@ var GeneratePython = &cobra.Command{ pusher_client = pusher.Pusher(app_id='` + commands.AppID + `', key='` + token.Key + `', secret='` + token.Secret + `', - host='api-` + app.Cluster + `.pusher.com', + host='` + apiHost(app.Cluster) + `', ssl=True ) diff --git a/commands/channels/list_channels.go b/commands/channels/list_channels.go index e443797..395960a 100644 --- a/commands/channels/list_channels.go +++ b/commands/channels/list_channels.go @@ -9,6 +9,7 @@ import ( "github.com/pusher/cli/commands" "github.com/pusher/pusher-http-go" "github.com/spf13/cobra" + "github.com/theherk/viper" ) var ListChannels = &cobra.Command{ @@ -23,14 +24,15 @@ var ListChannels = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { - fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) @@ -42,6 +44,7 @@ var ListChannels = &cobra.Command{ Key: token.Key, Secret: token.Secret, Cluster: app.Cluster, + Host: viper.GetString("apihost"), } opts := map[string]string{} diff --git a/commands/channels/subscribe.go b/commands/channels/subscribe.go index eedc77e..0d68092 100644 --- a/commands/channels/subscribe.go +++ b/commands/channels/subscribe.go @@ -30,14 +30,15 @@ var Subscribe = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { - fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) @@ -47,7 +48,7 @@ var Subscribe = &cobra.Command{ pusher.New(token.Key) client := pusher.NewWithConfig(pusher.ClientConfig{ Scheme: "wss", - Host: "ws-" + app.Cluster + ".pusher.com", + Host: wsHost(app.Cluster), Port: "443", Key: token.Key, Secret: token.Secret, diff --git a/commands/channels/tokens.go b/commands/channels/tokens.go index ab8d17c..8efcf75 100644 --- a/commands/channels/tokens.go +++ b/commands/channels/tokens.go @@ -25,7 +25,8 @@ var Tokens = &cobra.Command{ return } - tokens, err := api.GetAllTokensForApp(commands.AppID) + p := api.NewPusherApi() + tokens, err := p.GetAllTokensForApp(commands.AppID) if err != nil { fmt.Printf("Failed to retrieve the list of tokens: %s\n", err.Error()) os.Exit(1) diff --git a/commands/channels/trigger.go b/commands/channels/trigger.go index e547de4..16e9199 100644 --- a/commands/channels/trigger.go +++ b/commands/channels/trigger.go @@ -8,6 +8,7 @@ import ( "github.com/pusher/cli/commands" "github.com/pusher/pusher-http-go" "github.com/spf13/cobra" + "github.com/theherk/viper" ) // Trigger allows the user to trigger an event on a particular channel. @@ -41,14 +42,15 @@ var Trigger = &cobra.Command{ return } - app, err := api.GetApp(commands.AppID) + p := api.NewPusherApi() + app, err := p.GetApp(commands.AppID) if err != nil { - fmt.Fprintf(os.Stderr, "Could not get app the app: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Could not get the app: %s\n", err.Error()) os.Exit(1) return } - token, err := api.GetToken(commands.AppID) + token, err := p.GetToken(commands.AppID) if err != nil { fmt.Fprintf(os.Stderr, "Could not get app token: %s\n", err.Error()) os.Exit(1) @@ -60,6 +62,7 @@ var Trigger = &cobra.Command{ Key: token.Key, Secret: token.Secret, Cluster: app.Cluster, + Host: viper.GetString("apihost"), } err = client.Trigger(commands.ChannelName, commands.EventName, commands.Message) diff --git a/commands/shared_flags.go b/commands/shared_flags.go index 5846033..aa514fb 100644 --- a/commands/shared_flags.go +++ b/commands/shared_flags.go @@ -8,3 +8,11 @@ var OutputAsJSON bool var FilterByPrefix string var FetchUserCount bool var FetchSubscriptionCount bool +var FunctionName string +var FunctionEvents []string +var FunctionConfigName string +var FunctionConfigDescription string +var FunctionConfigParamType string +var FunctionConfigContent string +var FunctionMode string +var Data string diff --git a/config/config.go b/config/config.go index 96a7c8a..9492c8e 100644 --- a/config/config.go +++ b/config/config.go @@ -36,7 +36,7 @@ func getConfigPath() string { return path.Join(getConfigDir(), "pusher.json") } -//Init sets the config files location and attempts to read it in. +// Init sets the config files location and attempts to read it in. func Init() { if _, err := os.Stat(getConfigDir()); os.IsNotExist(err) { err = os.Mkdir(getConfigDir(), os.ModeDir|0755) diff --git a/go.mod b/go.mod index bd57d94..b4288ec 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( github.com/fatih/color v1.13.0 github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/google/go-cmp v0.5.8 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect @@ -26,6 +26,6 @@ require ( github.com/theherk/viper v0.0.0-20171202031228-e0502e82247d golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/text v0.3.8 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 9b03353..c5d02de 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -205,6 +205,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -217,6 +218,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -253,6 +255,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -285,6 +288,7 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -304,6 +308,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -343,9 +348,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -353,8 +361,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -405,6 +414,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index f0f7e4e..4e691c7 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/pusher/cli/api" "github.com/pusher/cli/commands/auth" "github.com/pusher/cli/commands/channels" "github.com/pusher/cli/commands/cli" @@ -17,7 +18,13 @@ func main() { var Apps = &cobra.Command{Use: "apps", Short: "Manage your Channels Apps"} - Apps.AddCommand(channels.Apps, channels.Tokens, channels.Subscribe, channels.Trigger, channels.ListChannels, channels.ChannelInfo) + funcCmd, err := channels.NewFunctionsCommand(api.NewPusherApi()) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not execute command: %s\n", err.Error()) + os.Exit(1) + return + } + Apps.AddCommand(channels.Apps, channels.Tokens, channels.Subscribe, channels.Trigger, channels.ListChannels, channels.ChannelInfo, funcCmd) var Generate = &cobra.Command{Use: "generate", Short: "Generate a Channels client, server, or Authorisation server"} @@ -30,7 +37,7 @@ func main() { rootCmd.AddCommand(Channels) rootCmd.AddCommand(auth.Login, auth.Logout) rootCmd.AddCommand(cli.Version) - err := rootCmd.Execute() + err = rootCmd.Execute() if err != nil { fmt.Fprintf(os.Stderr, "Could not execute command: %s\n", err.Error()) os.Exit(1) diff --git a/mockgen.Dockerfile b/mockgen.Dockerfile new file mode 100644 index 0000000..95d1fbf --- /dev/null +++ b/mockgen.Dockerfile @@ -0,0 +1,8 @@ +FROM golang:1.15 + +RUN cd /tmp +RUN GO111MODULE=on go get github.com/golang/mock/mockgen@v1.6.0 +RUN cd - + +WORKDIR /src +ENTRYPOINT ["go", "generate", "-v", "./..."]