mirror of
https://github.com/kopia/kopia.git
synced 2026-10-25 15:48:47 -04:00
server: support for server control APIs and tooling (#1644)
This adds new set of APIs `/api/v1/control/*` which can be used to administratively control a running server. Once the server is started, the administrative user can control it using CLI commands: export KOPIA_SERVER_ADDRESS=... export KOPIA_SERVER_CERT_FINGERPRINT=... export KOPIA_SERVER_PASSWORD=... * `kopia server status` - displays status of sources managed by the server * `kopia server snapshot` - triggers server-side upload of snapshots for managed sources * `kopia server cancel` - cancels upload of snapshots for managed sources * `kopia server pause` - pauses scheduled snapshots for managed sources * `kopia server resume` - resumes scheduled snapshots for managed sources * `kopia server refresh` - causes server to resynchronize with externally-made changes, such as policies or new sources * `kopia server flush` - causes server to flush all pending writes * `kopia server shutdown` - graceful shutdown of the server Authentication uses new user `server-control` and is disabled by default. To enable it when starting the server, provide the password using one of the following methods: * `--server-control-password` * `--random-server-control-password` * `.htpasswd` file * `KOPIA_SERVER_CONTROL_PASSWORD` environment variable This change allows us to tighten the API security and remove some methods that UI user was able to call, but which were not needed.
This commit is contained in:
1 parent
33b6fb9cda
commit
2e9a57f0b4
23 files changed
+506
-156
No files matched your search
+11
-3
@@ -13,6 +13,7 @@ function newServerForRepo(repoID) {
|
||||
let runningServerProcess = null;
|
||||
let runningServerCertSHA256 = "";
|
||||
let runningServerPassword = "";
|
||||
let runningServerControlPassword = "";
|
||||
let runningServerAddress = "";
|
||||
let runningServerCertificate = "";
|
||||
let runningServerStatusDetails = {
|
||||
@@ -37,6 +38,7 @@ function newServerForRepo(repoID) {
|
||||
'--tls-print-server-cert',
|
||||
'--tls-generate-cert-name=localhost',
|
||||
'--random-password',
|
||||
'--random-server-control-password',
|
||||
'--tls-generate-cert',
|
||||
'--shutdown-on-stdin', // shutdown the server when parent dies
|
||||
'--address=localhost:0');
|
||||
@@ -65,7 +67,7 @@ function newServerForRepo(repoID) {
|
||||
const statusUpdated = this.raiseStatusUpdatedEvent.bind(this);
|
||||
|
||||
function pollOnce() {
|
||||
if (!runningServerAddress || !runningServerCertificate || !runningServerPassword) {
|
||||
if (!runningServerAddress || !runningServerCertificate || !runningServerPassword || !runningServerControlPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,9 +76,9 @@ function newServerForRepo(repoID) {
|
||||
host: "localhost",
|
||||
port: parseInt(new URL(runningServerAddress).port),
|
||||
method: "GET",
|
||||
path: "/api/v1/repo/status",
|
||||
path: "/api/v1/control/status",
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from("kopia" + ':' + runningServerPassword).toString('base64')
|
||||
'Authorization': 'Basic ' + Buffer.from("server-control" + ':' + runningServerControlPassword).toString('base64')
|
||||
}
|
||||
}, (resp) => {
|
||||
if (resp.statusCode === 200) {
|
||||
@@ -110,6 +112,7 @@ function newServerForRepo(repoID) {
|
||||
|
||||
runningServerAddress = "";
|
||||
runningServerPassword = "";
|
||||
runningServerControlPassword = "";
|
||||
runningServerCertSHA256 = "";
|
||||
runningServerProcess = null;
|
||||
this.raiseStatusUpdatedEvent();
|
||||
@@ -133,6 +136,11 @@ function newServerForRepo(repoID) {
|
||||
this.raiseStatusUpdatedEvent();
|
||||
break;
|
||||
|
||||
case "SERVER CONTROL PASSWORD":
|
||||
runningServerControlPassword = value;
|
||||
this.raiseStatusUpdatedEvent();
|
||||
break;
|
||||
|
||||
case "SERVER CERT SHA256":
|
||||
runningServerCertSHA256 = value;
|
||||
this.raiseStatusUpdatedEvent();
|
||||
|
||||
+35
-20
@@ -8,16 +8,17 @@ import (
|
||||
)
|
||||
|
||||
type commandServer struct {
|
||||
acl commandServerACL
|
||||
user commandServerUser
|
||||
cancel commandServerCancel
|
||||
flush commandServerFlush
|
||||
pause commandServerPause
|
||||
refresh commandServerRefresh
|
||||
resume commandServerResume
|
||||
start commandServerStart
|
||||
status commandServerStatus
|
||||
upload commandServerUpload
|
||||
acl commandServerACL
|
||||
user commandServerUser
|
||||
cancel commandServerCancel
|
||||
flush commandServerFlush
|
||||
pause commandServerPause
|
||||
refresh commandServerRefresh
|
||||
resume commandServerResume
|
||||
start commandServerStart
|
||||
status commandServerStatus
|
||||
upload commandServerUpload
|
||||
shutdown commandServerShutdown
|
||||
}
|
||||
|
||||
type serverFlags struct {
|
||||
@@ -33,28 +34,42 @@ func (c *serverFlags) setup(cmd *kingpin.CmdClause) {
|
||||
}
|
||||
|
||||
type serverClientFlags struct {
|
||||
serverFlags
|
||||
serverAddress string
|
||||
serverUsername string
|
||||
serverPassword string
|
||||
serverCertFingerprint string
|
||||
}
|
||||
|
||||
func (c *serverClientFlags) setup(cmd *kingpin.CmdClause) {
|
||||
c.serverFlags.setup(cmd)
|
||||
cmd.Flag("server-cert-fingerprint", "Server certificate fingerprint").StringVar(&c.serverCertFingerprint)
|
||||
c.serverUsername = "server-control"
|
||||
|
||||
cmd.Flag("address", "Address of the server to connect to").Envar("KOPIA_SERVER_ADDRESS").Default("http://127.0.0.1:51515").StringVar(&c.serverAddress)
|
||||
cmd.Flag("server-control-username", "Server control username").Envar("KOPIA_SERVER_USERNAME").StringVar(&c.serverUsername)
|
||||
cmd.Flag("server-control-password", "Server control password").PlaceHolder("PASSWORD").Envar("KOPIA_SERVER_PASSWORD").StringVar(&c.serverPassword)
|
||||
|
||||
// aliases for backwards compat
|
||||
cmd.Flag("server-username", "Server control username").Hidden().StringVar(&c.serverUsername)
|
||||
cmd.Flag("server-password", "Server control password").Hidden().StringVar(&c.serverPassword)
|
||||
|
||||
cmd.Flag("server-cert-fingerprint", "Server certificate fingerprint").PlaceHolder("SHA256-FINGERPRINT").Envar("KOPIA_SERVER_CERT_FINGERPRINT").StringVar(&c.serverCertFingerprint)
|
||||
}
|
||||
|
||||
func (c *commandServer) setup(svc advancedAppServices, parent commandParent) {
|
||||
cmd := parent.Command("server", "Commands to control HTTP API server.")
|
||||
|
||||
c.cancel.setup(svc, cmd)
|
||||
c.flush.setup(svc, cmd)
|
||||
c.pause.setup(svc, cmd)
|
||||
c.refresh.setup(svc, cmd)
|
||||
c.resume.setup(svc, cmd)
|
||||
c.start.setup(svc, cmd)
|
||||
c.status.setup(svc, cmd)
|
||||
c.upload.setup(svc, cmd)
|
||||
c.acl.setup(svc, cmd)
|
||||
c.user.setup(svc, cmd)
|
||||
|
||||
c.status.setup(svc, cmd)
|
||||
c.refresh.setup(svc, cmd)
|
||||
c.flush.setup(svc, cmd)
|
||||
c.shutdown.setup(svc, cmd)
|
||||
|
||||
c.upload.setup(svc, cmd)
|
||||
c.cancel.setup(svc, cmd)
|
||||
c.pause.setup(svc, cmd)
|
||||
c.resume.setup(svc, cmd)
|
||||
}
|
||||
|
||||
func (c *serverClientFlags) serverAPIClientOptions() (apiclient.Options, error) {
|
||||
|
||||
@@ -4,20 +4,18 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/internal/apiclient"
|
||||
"github.com/kopia/kopia/internal/serverapi"
|
||||
)
|
||||
|
||||
type commandServerCancel struct {
|
||||
sf serverClientFlags
|
||||
commandServerSourceManagerAction
|
||||
}
|
||||
|
||||
func (c *commandServerCancel) setup(svc appServices, parent commandParent) {
|
||||
cmd := parent.Command("cancel", "Cancels in-progress uploads for one or more sources")
|
||||
c.sf.setup(cmd)
|
||||
c.commandServerSourceManagerAction.setup(svc, cmd)
|
||||
cmd.Action(svc.serverAction(&c.sf, c.runServerCancelUpload))
|
||||
}
|
||||
|
||||
func (c *commandServerCancel) runServerCancelUpload(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
// nolint:wrapcheck
|
||||
return cli.Post(ctx, "sources/cancel", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
return c.triggerActionOnMatchingSources(ctx, cli, "control/cancel-snapshot")
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/kopia/kopia/internal/testutil"
|
||||
"github.com/kopia/kopia/tests/testenv"
|
||||
)
|
||||
|
||||
func TestServerControl(t *testing.T) {
|
||||
env := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, testenv.NewInProcRunner(t))
|
||||
|
||||
dir0 := testutil.TempDirectory(t)
|
||||
dir1 := testutil.TempDirectory(t)
|
||||
dir2 := testutil.TempDirectory(t)
|
||||
dir3 := testutil.TempDirectory(t)
|
||||
|
||||
env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir, "--override-username=another-user", "--override-hostname=another-host")
|
||||
env.RunAndExpectSuccess(t, "snap", "create", dir0)
|
||||
|
||||
env.RunAndExpectSuccess(t, "repo", "connect", "filesystem", "--path", env.RepoDir, "--override-username=test-user", "--override-hostname=test-host")
|
||||
env.RunAndExpectSuccess(t, "snap", "create", dir1)
|
||||
env.RunAndExpectSuccess(t, "snap", "create", dir2)
|
||||
|
||||
serverStarted := make(chan struct{})
|
||||
serverStopped := make(chan struct{})
|
||||
|
||||
// detected from running server
|
||||
var (
|
||||
serverAddress string
|
||||
serverControlPassword string
|
||||
)
|
||||
|
||||
go func() {
|
||||
prefix := "SERVER ADDRESS: "
|
||||
passwordPrefix := "SERVER CONTROL PASSWORD: "
|
||||
|
||||
kill := env.RunAndProcessStderr(t, func(line string) bool {
|
||||
if strings.HasPrefix(line, passwordPrefix) {
|
||||
serverControlPassword = strings.TrimPrefix(line, passwordPrefix)
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
serverAddress = strings.TrimPrefix(line, prefix)
|
||||
close(serverStarted)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, "server", "start", "--insecure", "--random-server-control-password", "--address=127.0.0.1:0")
|
||||
defer kill()
|
||||
|
||||
close(serverStopped)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-serverStarted:
|
||||
t.Logf("server started on %v", serverAddress)
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("server did not start in time")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
lines := env.RunAndExpectSuccess(t, "server", "status", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
require.Len(t, lines, 2)
|
||||
require.Contains(t, lines, "IDLE: test-user@test-host:"+dir1)
|
||||
require.Contains(t, lines, "IDLE: test-user@test-host:"+dir2)
|
||||
|
||||
lines = env.RunAndExpectSuccess(t, "server", "status", "--address", serverAddress, "--server-control-password", serverControlPassword, "--remote")
|
||||
require.Len(t, lines, 3)
|
||||
require.Contains(t, lines, "IDLE: test-user@test-host:"+dir1)
|
||||
require.Contains(t, lines, "IDLE: test-user@test-host:"+dir2)
|
||||
require.Contains(t, lines, "REMOTE: another-user@another-host:"+dir0)
|
||||
|
||||
// create snapshot outside of the server
|
||||
env.RunAndExpectSuccess(t, "snap", "create", dir3)
|
||||
env.RunAndExpectSuccess(t, "server", "refresh", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
|
||||
lines = env.RunAndExpectSuccess(t, "server", "status", "--address", serverAddress, "--server-control-password", serverControlPassword, "--remote")
|
||||
require.Len(t, lines, 4)
|
||||
require.Contains(t, lines, "IDLE: test-user@test-host:"+dir3)
|
||||
|
||||
env.RunAndExpectSuccess(t, "server", "flush", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
|
||||
// trigger server snapshot
|
||||
env.RunAndExpectSuccess(t, "server", "snapshot", "--address", serverAddress, "--server-control-password", serverControlPassword, "--all")
|
||||
env.RunAndExpectSuccess(t, "server", "snapshot", "--address", serverAddress, "--server-control-password", serverControlPassword, dir1)
|
||||
env.RunAndExpectFailure(t, "server", "snapshot", "--address", serverAddress, "--server-control-password", serverControlPassword, "no-such-dir")
|
||||
|
||||
// neither dir nor --all specified
|
||||
env.RunAndExpectFailure(t, "server", "snapshot", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
|
||||
// cancel snapshot
|
||||
env.RunAndExpectSuccess(t, "server", "cancel", "--address", serverAddress, "--server-control-password", serverControlPassword, "--all")
|
||||
|
||||
env.RunAndExpectSuccess(t, "server", "pause", "--address", serverAddress, "--server-control-password", serverControlPassword, dir1)
|
||||
env.RunAndExpectSuccess(t, "server", "resume", "--address", serverAddress, "--server-control-password", serverControlPassword, dir1)
|
||||
|
||||
env.RunAndExpectSuccess(t, "server", "shutdown", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
|
||||
select {
|
||||
case <-serverStopped:
|
||||
t.Logf("server shut down")
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("server did not shutdown in time")
|
||||
}
|
||||
|
||||
// this will fail since the server is down
|
||||
env.RunAndExpectFailure(t, "server", "status", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
env.RunAndExpectFailure(t, "server", "flush", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
env.RunAndExpectFailure(t, "server", "refresh", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
env.RunAndExpectFailure(t, "server", "shutdown", "--address", serverAddress, "--server-control-password", serverControlPassword)
|
||||
}
|
||||
@@ -19,5 +19,5 @@ func (c *commandServerFlush) setup(svc appServices, parent commandParent) {
|
||||
|
||||
func (c *commandServerFlush) run(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
// nolint:wrapcheck
|
||||
return cli.Post(ctx, "flush", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
return cli.Post(ctx, "control/flush", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
}
|
||||
@@ -4,20 +4,18 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/internal/apiclient"
|
||||
"github.com/kopia/kopia/internal/serverapi"
|
||||
)
|
||||
|
||||
type commandServerPause struct {
|
||||
sf serverClientFlags
|
||||
commandServerSourceManagerAction
|
||||
}
|
||||
|
||||
func (c *commandServerPause) setup(svc appServices, parent commandParent) {
|
||||
cmd := parent.Command("pause", "Pause the scheduled snapshots for one or more sources")
|
||||
c.sf.setup(cmd)
|
||||
cmd.Action(svc.serverAction(&c.sf, runServerPause))
|
||||
c.commandServerSourceManagerAction.setup(svc, cmd)
|
||||
cmd.Action(svc.serverAction(&c.sf, c.run))
|
||||
}
|
||||
|
||||
func runServerPause(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
// nolint:wrapcheck
|
||||
return cli.Post(ctx, "sources/pause", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
func (c *commandServerPause) run(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
return c.triggerActionOnMatchingSources(ctx, cli, "control/pause-source")
|
||||
}
|
||||
@@ -19,5 +19,5 @@ func (c *commandServerRefresh) setup(svc appServices, parent commandParent) {
|
||||
|
||||
func (c *commandServerRefresh) run(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
// nolint:wrapcheck
|
||||
return cli.Post(ctx, "refresh", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
return cli.Post(ctx, "control/refresh", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
}
|
||||
@@ -4,20 +4,18 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/internal/apiclient"
|
||||
"github.com/kopia/kopia/internal/serverapi"
|
||||
)
|
||||
|
||||
type commandServerResume struct {
|
||||
sf serverClientFlags
|
||||
commandServerSourceManagerAction
|
||||
}
|
||||
|
||||
func (c *commandServerResume) setup(svc appServices, parent commandParent) {
|
||||
cmd := parent.Command("resume", "Resume the scheduled snapshots for one or more sources")
|
||||
c.sf.setup(cmd)
|
||||
cmd := parent.Command("resume", "Resume the scheduled snapshots for one or more sources").Alias("unpause")
|
||||
c.commandServerSourceManagerAction.setup(svc, cmd)
|
||||
cmd.Action(svc.serverAction(&c.sf, c.run))
|
||||
}
|
||||
|
||||
func (c *commandServerResume) run(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
// nolint:wrapcheck
|
||||
return cli.Post(ctx, "sources/resume", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
return c.triggerActionOnMatchingSources(ctx, cli, "control/resume-source")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/internal/apiclient"
|
||||
"github.com/kopia/kopia/internal/serverapi"
|
||||
)
|
||||
|
||||
type commandServerShutdown struct {
|
||||
sf serverClientFlags
|
||||
|
||||
out textOutput
|
||||
}
|
||||
|
||||
func (c *commandServerShutdown) setup(svc appServices, parent commandParent) {
|
||||
cmd := parent.Command("shutdown", "Gracefully shutdown the server")
|
||||
c.sf.setup(cmd)
|
||||
c.out.setup(svc)
|
||||
cmd.Action(svc.serverAction(&c.sf, c.run))
|
||||
}
|
||||
|
||||
func (c *commandServerShutdown) run(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
// nolint:wrapcheck
|
||||
return cli.Post(ctx, "control/shutdown", &serverapi.Empty{}, &serverapi.Empty{})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/internal/apiclient"
|
||||
)
|
||||
|
||||
type commandServerUpload struct {
|
||||
commandServerSourceManagerAction
|
||||
}
|
||||
|
||||
func (c *commandServerUpload) setup(svc appServices, parent commandParent) {
|
||||
cmd := parent.Command("snapshot", "Trigger upload for one or more existing sources").Alias("upload")
|
||||
|
||||
c.commandServerSourceManagerAction.setup(svc, cmd)
|
||||
cmd.Action(svc.serverAction(&c.sf, c.run))
|
||||
}
|
||||
|
||||
func (c *commandServerUpload) run(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
return c.triggerActionOnMatchingSources(ctx, cli, "control/trigger-snapshot")
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alecthomas/kingpin"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kopia/kopia/internal/apiclient"
|
||||
"github.com/kopia/kopia/internal/serverapi"
|
||||
)
|
||||
|
||||
// commandServerSourceManagerAction encapsulates common logic for all commands
|
||||
// that operate on snapshot sources managed by the server and can be used to
|
||||
// act upon them one-by-one or all at the same time.
|
||||
type commandServerSourceManagerAction struct {
|
||||
sf serverClientFlags
|
||||
|
||||
source string
|
||||
all bool
|
||||
|
||||
out textOutput
|
||||
}
|
||||
|
||||
func (c *commandServerSourceManagerAction) setup(svc appServices, cmd *kingpin.CmdClause) {
|
||||
cmd.Flag("all", "All paths managed by server").BoolVar(&c.all)
|
||||
cmd.Arg("source", "Source path managed by server").StringVar(&c.source)
|
||||
|
||||
c.sf.setup(cmd)
|
||||
c.out.setup(svc)
|
||||
}
|
||||
|
||||
func (c *commandServerSourceManagerAction) triggerActionOnMatchingSources(ctx context.Context, cli *apiclient.KopiaAPIClient, path string) error {
|
||||
var resp serverapi.MultipleSourceActionResponse
|
||||
|
||||
uv := url.Values{}
|
||||
|
||||
if !c.all {
|
||||
if c.source == "" {
|
||||
return errors.Errorf("must specify source or --all")
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(c.source)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to determine absolute path")
|
||||
}
|
||||
|
||||
uv.Set("path", absPath)
|
||||
}
|
||||
|
||||
if err := cli.Post(ctx, path+"?"+uv.Encode(), &serverapi.Empty{}, &resp); err != nil {
|
||||
return errors.Wrapf(err, "server returned error")
|
||||
}
|
||||
|
||||
for src, resp := range resp.Sources {
|
||||
if resp.Success {
|
||||
log(ctx).Infof("Success %v", src)
|
||||
} else {
|
||||
log(ctx).Warnf("Failed %v", src)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -43,6 +43,10 @@ type commandServerStart struct {
|
||||
serverStartRandomPassword bool
|
||||
serverStartHtpasswdFile string
|
||||
|
||||
randomServerControlPassword bool
|
||||
serverControlUsername string
|
||||
serverControlPassword string
|
||||
|
||||
serverAuthCookieSingingKey string
|
||||
|
||||
serverStartShutdownWhenStdinClosed bool
|
||||
@@ -80,6 +84,10 @@ func (c *commandServerStart) setup(svc advancedAppServices, parent commandParent
|
||||
cmd.Flag("random-password", "Generate random password and print to stderr").Hidden().BoolVar(&c.serverStartRandomPassword)
|
||||
cmd.Flag("htpasswd-file", "Path to htpasswd file that contains allowed user@hostname entries").Hidden().ExistingFileVar(&c.serverStartHtpasswdFile)
|
||||
|
||||
cmd.Flag("random-server-control-password", "Generate random server control password and print to stderr").Hidden().BoolVar(&c.randomServerControlPassword)
|
||||
cmd.Flag("server-control-username", "Server control username").Default("server-control").Envar("KOPIA_SERVER_CONTROL_USER").StringVar(&c.serverControlUsername)
|
||||
cmd.Flag("server-control-password", "Server control password").PlaceHolder("PASSWORD").Envar("KOPIA_SERVER_CONTROL_PASSWORD").StringVar(&c.serverControlPassword)
|
||||
|
||||
cmd.Flag("auth-cookie-signing-key", "Force particular auth cookie signing key").Envar("KOPIA_AUTH_COOKIE_SIGNING_KEY").Hidden().StringVar(&c.serverAuthCookieSingingKey)
|
||||
|
||||
cmd.Flag("shutdown-on-stdin", "Shut down the server when stdin handle has closed.").Hidden().BoolVar(&c.serverStartShutdownWhenStdinClosed)
|
||||
@@ -129,6 +137,7 @@ func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error
|
||||
Authorizer: auth.DefaultAuthorizer(),
|
||||
AuthCookieSigningKey: c.serverAuthCookieSingingKey,
|
||||
UIUser: c.sf.serverUsername,
|
||||
ServerControlUser: c.serverControlUsername,
|
||||
LogRequests: c.logServerRequests,
|
||||
PasswordPersist: c.svc.passwordPersistenceStrategy(),
|
||||
UIPreferencesFile: uiPreferencesFile,
|
||||
@@ -277,6 +286,24 @@ func (c *commandServerStart) getAuthenticator(ctx context.Context) (auth.Authent
|
||||
authenticators = append(authenticators, auth.AuthenticateSingleUser(c.sf.serverUsername, randomPassword))
|
||||
}
|
||||
|
||||
// handle server control password
|
||||
switch {
|
||||
case c.serverControlPassword != "":
|
||||
authenticators = append(authenticators, auth.AuthenticateSingleUser(c.serverControlUsername, c.serverControlPassword))
|
||||
|
||||
case c.randomServerControlPassword:
|
||||
// generate very long random one-time password
|
||||
b := make([]byte, serverRandomPasswordLength)
|
||||
io.ReadFull(rand.Reader, b) //nolint:errcheck
|
||||
|
||||
randomPassword := hex.EncodeToString(b)
|
||||
|
||||
// print it to the stderr bypassing any log file so that the user or calling process can connect
|
||||
fmt.Fprintln(c.out.stderr(), "SERVER CONTROL PASSWORD:", randomPassword)
|
||||
|
||||
authenticators = append(authenticators, auth.AuthenticateSingleUser(c.serverControlUsername, randomPassword))
|
||||
}
|
||||
|
||||
log(ctx).Infof(`
|
||||
Server will allow connections from users whose accounts are stored in the repository.
|
||||
User accounts can be added using 'kopia server user add'.
|
||||
|
||||
@@ -13,11 +13,15 @@ type commandServerStatus struct {
|
||||
sf serverClientFlags
|
||||
|
||||
out textOutput
|
||||
|
||||
remote bool
|
||||
}
|
||||
|
||||
func (c *commandServerStatus) setup(svc appServices, parent commandParent) {
|
||||
cmd := parent.Command("status", "Status of Kopia server")
|
||||
|
||||
cmd.Flag("remote", "Show remote sources").BoolVar(&c.remote)
|
||||
|
||||
c.sf.setup(cmd)
|
||||
c.out.setup(svc)
|
||||
|
||||
@@ -26,12 +30,16 @@ func (c *commandServerStatus) setup(svc appServices, parent commandParent) {
|
||||
|
||||
func (c *commandServerStatus) runServerStatus(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
var status serverapi.SourcesResponse
|
||||
if err := cli.Get(ctx, "sources", nil, &status); err != nil {
|
||||
if err := cli.Get(ctx, "control/sources", nil, &status); err != nil {
|
||||
return errors.Wrap(err, "unable to list sources")
|
||||
}
|
||||
|
||||
for _, src := range status.Sources {
|
||||
c.out.printStdout("%15v %v\n", src.Status, src.Source)
|
||||
if src.Status == "REMOTE" && !c.remote {
|
||||
continue
|
||||
}
|
||||
|
||||
c.out.printStdout("%v: %v\n", src.Status, src.Source)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kopia/kopia/internal/apiclient"
|
||||
"github.com/kopia/kopia/internal/serverapi"
|
||||
)
|
||||
|
||||
type commandServerUpload struct {
|
||||
sf serverClientFlags
|
||||
|
||||
out textOutput
|
||||
}
|
||||
|
||||
func (c *commandServerUpload) setup(svc appServices, parent commandParent) {
|
||||
cmd := parent.Command("upload", "Trigger upload for one or more sources")
|
||||
c.sf.setup(cmd)
|
||||
c.out.setup(svc)
|
||||
cmd.Action(svc.serverAction(&c.sf, c.run))
|
||||
}
|
||||
|
||||
func (c *commandServerUpload) run(ctx context.Context, cli *apiclient.KopiaAPIClient) error {
|
||||
return c.triggerActionOnMatchingSources(ctx, cli, "sources/upload")
|
||||
}
|
||||
|
||||
func (c *commandServerUpload) triggerActionOnMatchingSources(ctx context.Context, cli *apiclient.KopiaAPIClient, path string) error {
|
||||
var resp serverapi.MultipleSourceActionResponse
|
||||
|
||||
if err := cli.Post(ctx, path, &serverapi.Empty{}, &resp); err != nil {
|
||||
return errors.Wrapf(err, "unable to start upload on %v", path)
|
||||
}
|
||||
|
||||
for src, resp := range resp.Sources {
|
||||
if resp.Success {
|
||||
c.out.printStdout("SUCCESS %v\n", src)
|
||||
} else {
|
||||
c.out.printStdout("FAILED %v\n", src)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -93,7 +93,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kopia/htmluibuild v0.0.0-20211226001320-eed13dea3815
|
||||
github.com/kopia/htmluibuild v0.0.0-20220103200652-9a7b3d8f5556
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
|
||||
@@ -395,6 +395,8 @@ github.com/kopia/htmluibuild v0.0.0-20211223182507-6b6a50b2f0d1 h1:stwETPrkIzdLA
|
||||
github.com/kopia/htmluibuild v0.0.0-20211223182507-6b6a50b2f0d1/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw=
|
||||
github.com/kopia/htmluibuild v0.0.0-20211226001320-eed13dea3815 h1:tYyVCQw7MOqceTvuKIWQ2eCp/caJg5HRp1pO0Q68Z7Y=
|
||||
github.com/kopia/htmluibuild v0.0.0-20211226001320-eed13dea3815/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw=
|
||||
github.com/kopia/htmluibuild v0.0.0-20220103200652-9a7b3d8f5556 h1:ILqrzWYFuTpTAcAW5GMujLlnxs7v/cvlrpSjUWpUmco=
|
||||
github.com/kopia/htmluibuild v0.0.0-20220103200652-9a7b3d8f5556/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
|
||||
@@ -92,7 +92,6 @@ func (s *Server) APIHandlers(legacyAPI bool) http.Handler {
|
||||
m.HandleFunc("/api/v1/policies", s.handleAPI(requireUIUser, s.handlePolicyList)).Methods(http.MethodGet)
|
||||
|
||||
m.HandleFunc("/api/v1/refresh", s.handleAPI(anyAuthenticatedUser, s.handleRefresh)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/shutdown", s.handleAPIPossiblyNotConnected(requireUIUser, s.handleShutdown)).Methods(http.MethodPost)
|
||||
|
||||
m.HandleFunc("/api/v1/objects/{objectID}", s.requireAuth(s.handleObjectGet)).Methods(http.MethodGet)
|
||||
m.HandleFunc("/api/v1/restore", s.handleAPI(requireUIUser, s.handleRestore)).Methods(http.MethodPost)
|
||||
@@ -106,7 +105,7 @@ func (s *Server) APIHandlers(legacyAPI bool) http.Handler {
|
||||
|
||||
// methods that can be called by any authenticated user (UI or remote user).
|
||||
m.HandleFunc("/api/v1/flush", s.handleAPI(anyAuthenticatedUser, s.handleFlush)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/repo/status", s.handleAPIPossiblyNotConnected(anyAuthenticatedUser, s.handleRepoStatus)).Methods(http.MethodGet)
|
||||
m.HandleFunc("/api/v1/repo/status", s.handleAPIPossiblyNotConnected(requireUIUser, s.handleRepoStatus)).Methods(http.MethodGet)
|
||||
m.HandleFunc("/api/v1/repo/sync", s.handleAPI(anyAuthenticatedUser, s.handleRepoSync)).Methods(http.MethodPost)
|
||||
|
||||
m.HandleFunc("/api/v1/repo/connect", s.handleAPIPossiblyNotConnected(requireUIUser, s.handleRepoConnect)).Methods(http.MethodPost)
|
||||
@@ -147,6 +146,17 @@ func (s *Server) APIHandlers(legacyAPI bool) http.Handler {
|
||||
m.HandleFunc("/api/v1/tasks/{taskID}/logs", s.handleAPI(requireUIUser, s.handleTaskLogs)).Methods(http.MethodGet)
|
||||
m.HandleFunc("/api/v1/tasks/{taskID}/cancel", s.handleAPI(requireUIUser, s.handleTaskCancel)).Methods(http.MethodPost)
|
||||
|
||||
// server control API, requires authentication as `server-control`.
|
||||
m.HandleFunc("/api/v1/control/status", s.handleAPIPossiblyNotConnected(requireServerControlUser, s.handleRepoStatus)).Methods(http.MethodGet)
|
||||
m.HandleFunc("/api/v1/control/sources", s.handleAPI(requireServerControlUser, s.handleSourcesList)).Methods(http.MethodGet)
|
||||
m.HandleFunc("/api/v1/control/flush", s.handleAPIPossiblyNotConnected(requireServerControlUser, s.handleFlush)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/control/refresh", s.handleAPIPossiblyNotConnected(requireServerControlUser, s.handleRefresh)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/control/shutdown", s.handleAPI(requireServerControlUser, s.handleShutdown)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/control/trigger-snapshot", s.handleAPI(requireServerControlUser, s.handleUpload)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/control/cancel-snapshot", s.handleAPI(requireServerControlUser, s.handleCancel)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/control/pause-source", s.handleAPI(requireServerControlUser, s.handlePause)).Methods(http.MethodPost)
|
||||
m.HandleFunc("/api/v1/control/resume-source", s.handleAPI(requireServerControlUser, s.handleResume)).Methods(http.MethodPost)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -400,6 +410,10 @@ func (s *Server) forAllSourceManagersMatchingURLFilter(ctx context.Context, c fu
|
||||
}
|
||||
|
||||
for src, mgr := range s.sourceManagers {
|
||||
if mgr.isReadOnly {
|
||||
continue
|
||||
}
|
||||
|
||||
if !sourceMatchesURLFilter(src, values) {
|
||||
continue
|
||||
}
|
||||
@@ -407,6 +421,10 @@ func (s *Server) forAllSourceManagersMatchingURLFilter(ctx context.Context, c fu
|
||||
resp.Sources[src.String()] = c(mgr, ctx)
|
||||
}
|
||||
|
||||
if len(resp.Sources) == 0 {
|
||||
return nil, notFoundError("no source matching the provided filters")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -418,6 +436,14 @@ func (s *Server) handleCancel(ctx context.Context, r *http.Request, body []byte)
|
||||
return s.forAllSourceManagersMatchingURLFilter(ctx, (*sourceManager).cancel, r.URL.Query())
|
||||
}
|
||||
|
||||
func (s *Server) handlePause(ctx context.Context, r *http.Request, body []byte) (interface{}, *apiError) {
|
||||
return s.forAllSourceManagersMatchingURLFilter(ctx, (*sourceManager).pause, r.URL.Query())
|
||||
}
|
||||
|
||||
func (s *Server) handleResume(ctx context.Context, r *http.Request, body []byte) (interface{}, *apiError) {
|
||||
return s.forAllSourceManagersMatchingURLFilter(ctx, (*sourceManager).resume, r.URL.Query())
|
||||
}
|
||||
|
||||
func (s *Server) beginUpload(ctx context.Context, src snapshot.SourceInfo) {
|
||||
log(ctx).Debugf("waiting on semaphore to upload %v", src)
|
||||
s.uploadSemaphore <- struct{}{}
|
||||
@@ -697,6 +723,10 @@ func (s *Server) ServeStaticFiles(fs http.FileSystem) http.Handler {
|
||||
r = r2
|
||||
}
|
||||
|
||||
if !s.isAuthenticated(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
if !requireUIUser(s, r) {
|
||||
http.Error(w, `UI Access denied. See https://github.com/kopia/kopia/issues/880#issuecomment-798421751 for more information.`, http.StatusForbidden)
|
||||
return
|
||||
@@ -724,6 +754,7 @@ type Options struct {
|
||||
LogRequests bool
|
||||
UIUser string // name of the user allowed to access the UI
|
||||
UIPreferencesFile string // name of the JSON file storing UI preferences
|
||||
ServerControlUser string // name of the user allowed to access the server control API
|
||||
DisableCSRFTokenChecks bool
|
||||
UITitlePrefix string
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ func requireUIUser(s *Server, r *http.Request) bool {
|
||||
return user == s.options.UIUser
|
||||
}
|
||||
|
||||
func requireServerControlUser(s *Server, r *http.Request) bool {
|
||||
if s.authenticator == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
user, _, _ := r.BasicAuth()
|
||||
|
||||
return user == s.options.ServerControlUser
|
||||
}
|
||||
|
||||
func anyAuthenticatedUser(s *Server, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ type sourceManager struct {
|
||||
lastSnapshot *snapshot.Manifest
|
||||
lastCompleteSnapshot *snapshot.Manifest
|
||||
manifestsSinceLastCompleteSnapshot []*snapshot.Manifest
|
||||
paused bool
|
||||
isReadOnly bool
|
||||
|
||||
progress *snapshotfs.CountingUploadProgress
|
||||
currentTask string
|
||||
@@ -132,7 +134,12 @@ func (s *sourceManager) runLocal(ctx context.Context) {
|
||||
waitTime = oneDay
|
||||
}
|
||||
|
||||
s.setStatus("IDLE")
|
||||
if s.paused {
|
||||
s.setStatus("PAUSED")
|
||||
} else {
|
||||
s.setStatus("IDLE")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.closed:
|
||||
return
|
||||
@@ -174,6 +181,7 @@ func (s *sourceManager) backoffBeforeNextSnapshot() {
|
||||
}
|
||||
|
||||
func (s *sourceManager) runReadOnly(ctx context.Context) {
|
||||
s.isReadOnly = true
|
||||
s.refreshStatus(ctx)
|
||||
s.setStatus("REMOTE")
|
||||
|
||||
@@ -202,7 +210,7 @@ func (s *sourceManager) upload(ctx context.Context) serverapi.SourceActionRespon
|
||||
}
|
||||
|
||||
func (s *sourceManager) cancel(ctx context.Context) serverapi.SourceActionResponse {
|
||||
log(ctx).Infof("cancel triggered via API: %v", s.src)
|
||||
log(ctx).Debugw("cancel triggered via API", "source", s.src)
|
||||
|
||||
if u := s.currentUploader(); u != nil {
|
||||
log(ctx).Infof("canceling current upload")
|
||||
@@ -212,6 +220,41 @@ func (s *sourceManager) cancel(ctx context.Context) serverapi.SourceActionRespon
|
||||
return serverapi.SourceActionResponse{Success: true}
|
||||
}
|
||||
|
||||
func (s *sourceManager) pause(ctx context.Context) serverapi.SourceActionResponse {
|
||||
log(ctx).Debugw("pause triggered via API", "source", s.src)
|
||||
|
||||
s.mu.Lock()
|
||||
s.paused = true
|
||||
s.mu.Unlock()
|
||||
|
||||
if u := s.currentUploader(); u != nil {
|
||||
log(ctx).Infof("canceling current upload")
|
||||
u.Cancel()
|
||||
} else {
|
||||
select {
|
||||
case s.refreshRequested <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return serverapi.SourceActionResponse{Success: true}
|
||||
}
|
||||
|
||||
func (s *sourceManager) resume(ctx context.Context) serverapi.SourceActionResponse {
|
||||
log(ctx).Debugw("resume triggered via API", "source", s.src)
|
||||
|
||||
s.mu.Lock()
|
||||
s.paused = false
|
||||
s.mu.Unlock()
|
||||
|
||||
select {
|
||||
case s.refreshRequested <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
return serverapi.SourceActionResponse{Success: true}
|
||||
}
|
||||
|
||||
func (s *sourceManager) stop(ctx context.Context) {
|
||||
log(ctx).Debugf("stopping source manager for %v", s.src)
|
||||
|
||||
@@ -369,7 +412,11 @@ func (s *sourceManager) refreshStatus(ctx context.Context) {
|
||||
s.lastSnapshot = nil
|
||||
}
|
||||
|
||||
s.nextSnapshotTime = s.findClosestNextSnapshotTime()
|
||||
if s.paused {
|
||||
s.nextSnapshotTime = nil
|
||||
} else {
|
||||
s.nextSnapshotTime = s.findClosestNextSnapshotTime()
|
||||
}
|
||||
}
|
||||
|
||||
type uitaskProgress struct {
|
||||
|
||||
@@ -83,16 +83,16 @@ func DisconnectFromRepository(ctx context.Context, c *apiclient.KopiaAPIClient)
|
||||
return c.Post(ctx, "repo/disconnect", &Empty{}, &Empty{})
|
||||
}
|
||||
|
||||
// Shutdown invokes the 'repo/shutdown' API.
|
||||
// Shutdown invokes the 'control/shutdown' API.
|
||||
func Shutdown(ctx context.Context, c *apiclient.KopiaAPIClient) error {
|
||||
// nolint:wrapcheck
|
||||
return c.Post(ctx, "shutdown", &Empty{}, &Empty{})
|
||||
return c.Post(ctx, "control/shutdown", &Empty{}, &Empty{})
|
||||
}
|
||||
|
||||
// Status invokes the 'repo/status' API.
|
||||
// Status invokes the 'control/status' API.
|
||||
func Status(ctx context.Context, c *apiclient.KopiaAPIClient) (*StatusResponse, error) {
|
||||
resp := &StatusResponse{}
|
||||
if err := c.Get(ctx, "repo/status", nil, resp); err != nil {
|
||||
if err := c.Get(ctx, "control/status", nil, resp); err != nil {
|
||||
return nil, errors.Wrap(err, "Status")
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ func TestACL(t *testing.T) {
|
||||
kill := serverEnvironment.RunAndProcessStderr(t, sp.ProcessOutput,
|
||||
"server", "start",
|
||||
"--address=localhost:0",
|
||||
"--server-username=admin-user",
|
||||
"--server-password=admin-pwd",
|
||||
"--server-control-username=admin-user",
|
||||
"--server-control-password=admin-pwd",
|
||||
"--tls-generate-cert",
|
||||
"--tls-generate-rsa-key-size=2048", // use shorter key size to speed up generation
|
||||
)
|
||||
@@ -126,11 +126,8 @@ func TestACL(t *testing.T) {
|
||||
"--server-cert-fingerprint", sp.sha256Fingerprint,
|
||||
)
|
||||
|
||||
// we could use foo@bar's credentials as well (any valid user/password will do)
|
||||
// since the password was and is valid, but using alice@wonderland with password 'baz'
|
||||
// is not reliable since we don't know whether the server has already picked up the
|
||||
// change or is still using the old password.
|
||||
serverEnvironment.RunAndExpectSuccess(t, "server", "refresh",
|
||||
// attempt to use foo@bar's credentials when refreshing, this will fail.
|
||||
serverEnvironment.RunAndExpectFailure(t, "server", "refresh",
|
||||
"--address", sp.baseURL,
|
||||
"--server-username", "foo@bar",
|
||||
"--server-password", "baz",
|
||||
|
||||
@@ -25,6 +25,9 @@ var htpasswdFileContents = []byte("foo@bar:$2y$05$JWrExvBe5Knh0.AMLk5WHu.EzfOP.L
|
||||
const (
|
||||
uiUsername = "ui-user-password"
|
||||
uiPassword = "ui-password"
|
||||
|
||||
controlUsername = "control-user-password"
|
||||
controlPassword = "control-password"
|
||||
)
|
||||
|
||||
func TestAPIServerRepository_GRPC_htpasswd(t *testing.T) {
|
||||
@@ -101,13 +104,15 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
|
||||
"--tls-cert-file", tlsCert,
|
||||
"--server-username", uiUsername,
|
||||
"--server-password", uiPassword,
|
||||
"--server-control-username", controlUsername,
|
||||
"--server-control-password", controlPassword,
|
||||
}, serverStartArgs...)...)
|
||||
t.Logf("detected server parameters %#v", sp)
|
||||
|
||||
cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
|
||||
controlClient, err := apiclient.NewKopiaAPIClient(apiclient.Options{
|
||||
BaseURL: sp.baseURL,
|
||||
Username: "foo@bar",
|
||||
Password: "baz",
|
||||
Username: controlUsername,
|
||||
Password: controlPassword,
|
||||
TrustedServerCertificateFingerprint: sp.sha256Fingerprint,
|
||||
LogRequests: true,
|
||||
})
|
||||
@@ -115,18 +120,7 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
|
||||
t.Fatalf("unable to create API apiclient")
|
||||
}
|
||||
|
||||
uiUserCLI, err := apiclient.NewKopiaAPIClient(apiclient.Options{
|
||||
BaseURL: sp.baseURL,
|
||||
Username: uiUsername,
|
||||
Password: uiPassword,
|
||||
TrustedServerCertificateFingerprint: sp.sha256Fingerprint,
|
||||
LogRequests: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create API apiclient")
|
||||
}
|
||||
|
||||
waitUntilServerStarted(ctx, t, cli)
|
||||
waitUntilServerStarted(ctx, t, controlClient)
|
||||
|
||||
// open repository client.
|
||||
rep, err := repo.OpenAPIServer(ctx, &repo.APIServerInfo{
|
||||
@@ -148,7 +142,7 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
logErrorAndIgnore(t, serverapi.Shutdown(ctx, uiUserCLI))
|
||||
logErrorAndIgnore(t, serverapi.Shutdown(ctx, controlClient))
|
||||
|
||||
// give the server a moment to wind down.
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -165,23 +159,14 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
|
||||
"--tls-cert-file", tlsCert,
|
||||
"--server-username", uiUsername,
|
||||
"--server-password", uiPassword,
|
||||
"--server-control-username", controlUsername,
|
||||
"--server-control-password", controlPassword,
|
||||
}, serverStartArgs...)...)
|
||||
t.Logf("detected server parameters %#v", sp)
|
||||
|
||||
cli, err = apiclient.NewKopiaAPIClient(apiclient.Options{
|
||||
BaseURL: sp.baseURL,
|
||||
Username: "foo@bar",
|
||||
Password: "baz",
|
||||
TrustedServerCertificateFingerprint: sp.sha256Fingerprint,
|
||||
LogRequests: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create API apiclient")
|
||||
}
|
||||
waitUntilServerStarted(ctx, t, controlClient)
|
||||
|
||||
waitUntilServerStarted(ctx, t, cli)
|
||||
|
||||
defer serverapi.Shutdown(ctx, uiUserCLI)
|
||||
defer serverapi.Shutdown(ctx, controlClient)
|
||||
|
||||
someLabels := map[string]string{
|
||||
"type": "snapshot",
|
||||
@@ -261,7 +246,7 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
|
||||
}
|
||||
|
||||
// shutdown the server
|
||||
logErrorAndIgnore(t, serverapi.Shutdown(ctx, uiUserCLI))
|
||||
logErrorAndIgnore(t, serverapi.Shutdown(ctx, controlClient))
|
||||
|
||||
// open repository client to a dead server, this should fail quickly instead of retrying forever.
|
||||
timer := timetrack.StartTimer()
|
||||
|
||||
@@ -27,15 +27,17 @@ import (
|
||||
|
||||
// Pattern in stderr that `kopia server` uses to pass ephemeral data.
|
||||
const (
|
||||
serverOutputAddress = "SERVER ADDRESS: "
|
||||
serverOutputCertSHA256 = "SERVER CERT SHA256: "
|
||||
serverOutputPassword = "SERVER PASSWORD: "
|
||||
serverOutputAddress = "SERVER ADDRESS: "
|
||||
serverOutputCertSHA256 = "SERVER CERT SHA256: "
|
||||
serverOutputPassword = "SERVER PASSWORD: "
|
||||
serverOutputControlPassword = "SERVER CONTROL PASSWORD: "
|
||||
)
|
||||
|
||||
type serverParameters struct {
|
||||
baseURL string
|
||||
sha256Fingerprint string
|
||||
password string
|
||||
baseURL string
|
||||
sha256Fingerprint string
|
||||
password string
|
||||
serverControlPassword string
|
||||
}
|
||||
|
||||
func (s *serverParameters) ProcessOutput(l string) bool {
|
||||
@@ -52,6 +54,10 @@ func (s *serverParameters) ProcessOutput(l string) bool {
|
||||
s.password = strings.TrimPrefix(l, serverOutputPassword)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(l, serverOutputControlPassword) {
|
||||
s.serverControlPassword = strings.TrimPrefix(l, serverOutputControlPassword)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -75,6 +81,7 @@ func TestServerStart(t *testing.T) {
|
||||
"--ui",
|
||||
"--address=localhost:0",
|
||||
"--random-password",
|
||||
"--random-server-control-password",
|
||||
"--tls-generate-cert",
|
||||
"--tls-generate-rsa-key-size=2048", // use shorter key size to speed up generation
|
||||
"--override-hostname=fake-hostname",
|
||||
@@ -92,12 +99,21 @@ func TestServerStart(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer serverapi.Shutdown(ctx, cli)
|
||||
controlClient, err := apiclient.NewKopiaAPIClient(apiclient.Options{
|
||||
BaseURL: sp.baseURL,
|
||||
Username: "server-control",
|
||||
Password: sp.serverControlPassword,
|
||||
TrustedServerCertificateFingerprint: sp.sha256Fingerprint,
|
||||
LogRequests: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
waitUntilServerStarted(ctx, t, cli)
|
||||
defer serverapi.Shutdown(ctx, controlClient)
|
||||
|
||||
waitUntilServerStarted(ctx, t, controlClient)
|
||||
verifyUIServedWithCorrectTitle(t, cli, sp)
|
||||
|
||||
st := verifyServerConnected(t, cli, true)
|
||||
st := verifyServerConnected(t, controlClient, true)
|
||||
require.Equal(t, "filesystem", st.Storage)
|
||||
|
||||
limits, err := serverapi.GetThrottlingLimits(ctx, cli)
|
||||
@@ -205,6 +221,7 @@ func TestServerCreateAndConnectViaAPI(t *testing.T) {
|
||||
e.RunAndProcessStderr(t, sp.ProcessOutput,
|
||||
"server", "start", "--ui",
|
||||
"--address=localhost:0", "--random-password",
|
||||
"--random-server-control-password",
|
||||
"--tls-generate-cert",
|
||||
"--tls-generate-rsa-key-size=2048", // use shorter key size to speed up generation,
|
||||
)
|
||||
@@ -218,10 +235,19 @@ func TestServerCreateAndConnectViaAPI(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer serverapi.Shutdown(ctx, cli)
|
||||
controlClient, err := apiclient.NewKopiaAPIClient(apiclient.Options{
|
||||
BaseURL: sp.baseURL,
|
||||
Username: "server-control",
|
||||
Password: sp.serverControlPassword,
|
||||
TrustedServerCertificateFingerprint: sp.sha256Fingerprint,
|
||||
LogRequests: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
waitUntilServerStarted(ctx, t, cli)
|
||||
verifyServerConnected(t, cli, false)
|
||||
defer serverapi.Shutdown(ctx, controlClient)
|
||||
|
||||
waitUntilServerStarted(ctx, t, controlClient)
|
||||
verifyServerConnected(t, controlClient, false)
|
||||
|
||||
if err = serverapi.CreateRepository(ctx, cli, &serverapi.CreateRepositoryRequest{
|
||||
ConnectRepositoryRequest: serverapi.ConnectRepositoryRequest{
|
||||
@@ -232,13 +258,13 @@ func TestServerCreateAndConnectViaAPI(t *testing.T) {
|
||||
t.Fatalf("create error: %v", err)
|
||||
}
|
||||
|
||||
verifyServerConnected(t, cli, true)
|
||||
verifyServerConnected(t, controlClient, true)
|
||||
|
||||
if err = serverapi.DisconnectFromRepository(ctx, cli); err != nil {
|
||||
t.Fatalf("disconnect error: %v", err)
|
||||
}
|
||||
|
||||
verifyServerConnected(t, cli, false)
|
||||
verifyServerConnected(t, controlClient, false)
|
||||
|
||||
if err = serverapi.ConnectToRepository(ctx, cli, &serverapi.ConnectRepositoryRequest{
|
||||
Password: "foofoo",
|
||||
@@ -247,7 +273,7 @@ func TestServerCreateAndConnectViaAPI(t *testing.T) {
|
||||
t.Fatalf("create error: %v", err)
|
||||
}
|
||||
|
||||
verifyServerConnected(t, cli, true)
|
||||
verifyServerConnected(t, controlClient, true)
|
||||
}
|
||||
|
||||
func TestConnectToExistingRepositoryViaAPI(t *testing.T) {
|
||||
@@ -274,6 +300,7 @@ func TestConnectToExistingRepositoryViaAPI(t *testing.T) {
|
||||
// at this point repository is not connected, start the server
|
||||
e.RunAndProcessStderr(t, sp.ProcessOutput, "server", "start",
|
||||
"--ui", "--address=localhost:0", "--random-password",
|
||||
"--random-server-control-password",
|
||||
"--tls-generate-cert",
|
||||
"--tls-generate-rsa-key-size=2048", // use shorter key size to speed up generation
|
||||
"--override-hostname=fake-hostname", "--override-username=fake-username")
|
||||
@@ -287,10 +314,19 @@ func TestConnectToExistingRepositoryViaAPI(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer serverapi.Shutdown(ctx, cli)
|
||||
controlClient, err := apiclient.NewKopiaAPIClient(apiclient.Options{
|
||||
BaseURL: sp.baseURL,
|
||||
Username: "server-control",
|
||||
Password: sp.serverControlPassword,
|
||||
TrustedServerCertificateFingerprint: sp.sha256Fingerprint,
|
||||
LogRequests: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
waitUntilServerStarted(ctx, t, cli)
|
||||
verifyServerConnected(t, cli, false)
|
||||
defer serverapi.Shutdown(ctx, controlClient)
|
||||
|
||||
waitUntilServerStarted(ctx, t, controlClient)
|
||||
verifyServerConnected(t, controlClient, false)
|
||||
|
||||
if err = serverapi.ConnectToRepository(ctx, cli, &serverapi.ConnectRepositoryRequest{
|
||||
Password: testenv.TestRepoPassword,
|
||||
@@ -299,7 +335,7 @@ func TestConnectToExistingRepositoryViaAPI(t *testing.T) {
|
||||
t.Fatalf("connect error: %v", err)
|
||||
}
|
||||
|
||||
verifyServerConnected(t, cli, true)
|
||||
verifyServerConnected(t, controlClient, true)
|
||||
|
||||
si := snapshot.SourceInfo{Host: "fake-hostname", UserName: "fake-username", Path: sharedTestDataDir1}
|
||||
|
||||
|
||||
Reference in new issue
Block a user