Skip to content

Commit

Permalink
Merge branch 'dn2_instance_test' into unstable
Browse files Browse the repository at this point in the history
  • Loading branch information
davidnewhall committed Jul 21, 2024
2 parents dee2da7 + c21f03f commit 808ba84
Show file tree
Hide file tree
Showing 16 changed files with 489 additions and 438 deletions.
120 changes: 120 additions & 0 deletions pkg/checkapp/checkapp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Package checkapp provides a suite of small procedures to check integration URLs and commands.
// This is used by all the double-green check marks on the client UI.
package checkapp

import (
"context"
"errors"
"html"
"net/http"
"strconv"
"strings"

"github.com/Notifiarr/notifiarr/pkg/configfile"
"github.com/Notifiarr/notifiarr/pkg/mnd"
"github.com/Notifiarr/notifiarr/pkg/snapshot"
"github.com/gorilla/mux"
)

const (
success = "Connection Successful! Version: "
connecting = "Connecting: "
validation = "Validation: "
)

type Input struct {
Real *configfile.Config
Post *configfile.Config
Type string
Index int
}

var ErrBadIndex = errors.New("index provided has no configuration data")

func Test(orig *configfile.Config, writer http.ResponseWriter, req *http.Request) {
posted := configfile.Config{}

if err := mnd.ConfigPostDecoder.Decode(&posted, req.PostForm); err != nil {
http.Error(writer, "Decoding POST data into Go data structure failed: "+err.Error(), http.StatusBadRequest)
return
}

index, _ := strconv.Atoi(mux.Vars(req)["index"])
reply, code := testInstance(req.Context(), &Input{
Real: orig,
Post: &posted,
Type: mux.Vars(req)["type"],
Index: index,
})
http.Error(writer, html.EscapeString(reply), code)
}

//nolint:funlen,cyclop // It's really not that bad.
func testInstance(ctx context.Context, input *Input) (string, int) {
switch strings.ToLower(input.Type) {
// commands.go
case "commands":
return testCommand(ctx, input)
// downloaders.go
case "nzbget":
return checkAndRun(ctx, testNZBGet, input, input.Post.Apps, input.Post.Apps.NZBGet)
case "deluge":
return checkAndRun(ctx, testDeluge, input, input.Post.Apps, input.Post.Apps.Deluge)
case "qbit":
return checkAndRun(ctx, testQbit, input, input.Post.Apps, input.Post.Apps.Qbit)
case "rtorrent":
return checkAndRun(ctx, testRtorrent, input, input.Post.Apps, input.Post.Apps.Rtorrent)
case "transmission":
return checkAndRun(ctx, testTransmission, input, input.Post.Apps, input.Post.Apps.Transmission)
case "sabnzb":
return checkAndRun(ctx, testSabNZB, input, input.Post.Apps, input.Post.Apps.SabNZB)
// starr.go
case "lidarr":
return checkAndRun(ctx, testLidarr, input, input.Post.Apps, input.Post.Apps.Lidarr)
case "prowlarr":
return checkAndRun(ctx, testProwlarr, input, input.Post.Apps, input.Post.Apps.Prowlarr)
case "radarr":
return checkAndRun(ctx, testRadarr, input, input.Post.Apps, input.Post.Apps.Radarr)
case "readarr":
return checkAndRun(ctx, testReadarr, input, input.Post.Apps, input.Post.Apps.Readarr)
case "sonarr":
return checkAndRun(ctx, testSonarr, input, input.Post.Apps, input.Post.Apps.Sonarr)
// snapshots.go
case "mysql":
return checkAndRun(ctx, testMySQL, input, input.Post.Snapshot, input.Post.Snapshot.Plugins.MySQL)
case "nvidia":
return checkAndRun(ctx, testNvidia, input, input.Post.Snapshot,
[]*snapshot.NvidiaConfig{input.Post.Snapshot.Plugins.Nvidia}) // ad-hoc slice, index is already 0.
// services.go
case "tcp":
return checkAndRun(ctx, testTCP, input, input.Post.Service, input.Post.Service)
case "http":
return checkAndRun(ctx, testHTTP, input, input.Post.Service, input.Post.Service)
case "process":
return checkAndRun(ctx, testProcess, input, input.Post.Service, input.Post.Service)
case "ping", "icmp":
return checkAndRun(ctx, testPing, input, input.Post.Service, input.Post.Service)
// media.go
case "plex":
return testPlex(ctx, input.Post.Plex)
case "tautulli":
return testTautulli(ctx, input.Post.Apps.Tautulli)
default:
return "Unknown Check Type Requested! (" + input.Type + ")", http.StatusNotImplemented
}
}

// checkAndRun makes sure the slice length is at least as long as the index, and checks parents for nil.
func checkAndRun[D any](
ctx context.Context,
checker func(ctx context.Context, input D) (string, int),
input *Input,
parent any,
slice []D,
) (string, int) {
if parent == nil || len(slice) <= input.Index {
return ErrBadIndex.Error(), http.StatusBadRequest
}

return checker(ctx, slice[input.Index])
}
40 changes: 40 additions & 0 deletions pkg/checkapp/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package checkapp

import (
"context"
"fmt"
"net/http"

"github.com/Notifiarr/notifiarr/pkg/triggers/commands"
"github.com/Notifiarr/notifiarr/pkg/triggers/common"
"github.com/Notifiarr/notifiarr/pkg/website"
)

func testCommand(ctx context.Context, input *Input) (string, int) {
if len(input.Real.Commands) > input.Index {
input.Real.Commands[input.Index].Run(&common.ActionInput{Type: website.EventGUI})
return fmt.Sprintf("Command Triggered: %s", input.Real.Commands[input.Index].Name), http.StatusOK
} else if len(input.Post.Commands) > input.Index { // check POST input for "new" command.
input.Post.Commands[input.Index].Setup(input.Real.Logger, input.Real.Services.Website)

if err := input.Post.Commands[input.Index].SetupRegexpArgs(); err != nil {
return err.Error(), http.StatusInternalServerError
}

return testCustomCommand(ctx, input.Post.Commands[input.Index])
}

return ErrBadIndex.Error(), http.StatusBadRequest
}

func testCustomCommand(ctx context.Context, cmd *commands.Command) (string, int) {
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout.Duration)
defer cancel()

output, err := cmd.RunNow(ctx, &common.ActionInput{Type: website.EventGUI})
if err != nil {
return fmt.Sprintf("Command Failed! Error: %v", err), http.StatusInternalServerError
}

return fmt.Sprintf("Command Successful! Output: %s", output), http.StatusOK
}
98 changes: 98 additions & 0 deletions pkg/checkapp/downloaders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package checkapp

import (
"context"
"fmt"
"net/http"
"net/url"

"github.com/Notifiarr/notifiarr/pkg/apps"
"github.com/Notifiarr/notifiarr/pkg/mnd"
"github.com/hekmon/transmissionrpc/v3"
"golift.io/deluge"
"golift.io/nzbget"
"golift.io/qbit"
"golift.io/version"
)

func testQbit(ctx context.Context, config *apps.QbitConfig) (string, int) {
qbit, err := qbit.New(ctx, config.Config)
if err != nil {
return connecting + err.Error(), http.StatusBadGateway
}

xfers, err := qbit.GetXfersContext(ctx)
if err != nil {
return "Getting Transfers: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("Connection Successful! %d Transfers", len(xfers)), http.StatusOK
}

func testRtorrent(_ context.Context, config *apps.RtorrentConfig) (string, int) {
config.Setup(0, nil)

result, err := config.Client.Call("system.hostname")
if err != nil {
return "Getting Server Name: " + err.Error(), http.StatusBadGateway
}

if names, ok := result.([]any); ok {
result = names[0]
}

if name, ok := result.(string); ok {
return fmt.Sprintf("Connection Successful! Server name: %s", name), http.StatusOK
}

return "Getting Server Name: result was not a string?", http.StatusBadGateway
}

func testSabNZB(ctx context.Context, app *apps.SabNZBConfig) (string, int) {
app.Setup(0, nil)

sab, err := app.GetQueue(ctx)
if err != nil {
return "Getting Queue: " + err.Error(), http.StatusBadGateway
}

return success + sab.Version, http.StatusOK
}

func testNZBGet(ctx context.Context, config *apps.NZBGetConfig) (string, int) {
ver, err := nzbget.New(config.Config).VersionContext(ctx)
if err != nil {
return "Getting Version: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("%s%s", success, ver), http.StatusOK
}

func testDeluge(ctx context.Context, config *apps.DelugeConfig) (string, int) {
deluge, err := deluge.New(ctx, config.Config)
if err != nil {
return connecting + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("%s%s", success, deluge.Version), http.StatusOK
}

func testTransmission(ctx context.Context, config *apps.XmissionConfig) (string, int) {
endpoint, err := url.Parse(config.URL)
if err != nil {
return "parsing url: " + err.Error(), http.StatusBadGateway
} else if config.User != "" {
endpoint.User = url.UserPassword(config.User, config.Pass)
}

client, _ := transmissionrpc.New(endpoint, &transmissionrpc.Config{
UserAgent: fmt.Sprintf("%s v%s-%s %s", mnd.Title, version.Version, version.Revision, version.Branch),
})

args, err := client.SessionArgumentsGetAll(ctx)
if err != nil {
return "Getting Server Version: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintln("Transmission Server version:", *args.Version), http.StatusOK
}
31 changes: 31 additions & 0 deletions pkg/checkapp/media.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package checkapp

import (
"context"
"fmt"
"net/http"

"github.com/Notifiarr/notifiarr/pkg/apps"
)

func testPlex(ctx context.Context, app *apps.PlexConfig) (string, int) {
app.Setup(0, nil)

info, err := app.GetInfo(ctx)
if err != nil {
return "Getting Info: " + err.Error(), http.StatusBadGateway
}

return "Plex OK! Version: " + info.Version, http.StatusOK
}

func testTautulli(ctx context.Context, app *apps.TautulliConfig) (string, int) {
app.Setup(0, nil)

users, err := app.GetUsers(ctx)
if err != nil {
return "Getting Users: " + err.Error(), http.StatusBadGateway
}

return fmt.Sprintf("Tautulli OK! Users: %d", len(users.Response.Data)), http.StatusOK
}
61 changes: 61 additions & 0 deletions pkg/checkapp/services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package checkapp

import (
"context"
"net/http"

"github.com/Notifiarr/notifiarr/pkg/services"
)

func testTCP(ctx context.Context, svc *services.Service) (string, int) {
if err := svc.Validate(); err != nil {
return validation + err.Error(), http.StatusBadRequest
}

res := svc.CheckOnly(ctx)
if res.State != services.StateOK {
return res.State.String() + " " + res.Output.String(), http.StatusBadGateway
}

return "TCP Port is OPEN and reachable: " + res.Output.String(), http.StatusOK
}

func testHTTP(ctx context.Context, svc *services.Service) (string, int) {
if err := svc.Validate(); err != nil {
return validation + err.Error(), http.StatusBadRequest
}

res := svc.CheckOnly(ctx)
if res.State != services.StateOK {
return res.State.String() + " " + res.Output.String(), http.StatusBadGateway
}

// add test
return "HTTP Response Code Acceptable! " + res.Output.String(), http.StatusOK
}

func testProcess(ctx context.Context, svc *services.Service) (string, int) {
if err := svc.Validate(); err != nil {
return validation + err.Error(), http.StatusBadRequest
}

res := svc.CheckOnly(ctx)
if res.State != services.StateOK {
return res.State.String() + " " + res.Output.String(), http.StatusBadGateway
}

return "Process Tested OK: " + res.Output.String(), http.StatusOK
}

func testPing(ctx context.Context, svc *services.Service) (string, int) {
if err := svc.Validate(); err != nil {
return validation + err.Error(), http.StatusBadRequest
}

res := svc.CheckOnly(ctx)
if res.State != services.StateOK {
return res.State.String() + " " + res.Output.String(), http.StatusBadGateway
}

return "Ping Tested OK: " + res.Output.String(), http.StatusOK
}
Loading

0 comments on commit 808ba84

Please sign in to comment.