Skip to content
Merged
14 changes: 10 additions & 4 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -621,14 +621,14 @@ components:
CLIDeduplicatePlanItem:
additionalProperties: true
properties:
backfilled_count:
format: int64
type: integer
duplicate_messages:
format: int64
type: integer
needs_confirmation:
type: boolean
pending_backfill_count:
format: int64
type: integer
plan_fingerprint:
type: string
scope_is_collection:
Expand All @@ -654,8 +654,14 @@ components:
type: boolean
delete_dups_from_source_server:
type: boolean
plan_protocol:
enum:
- explicit-backfill-v1
type: string
prefer:
type: string
required:
- plan_protocol
type: object
CLIDeduplicatePlanResponse:
additionalProperties: true
Expand Down Expand Up @@ -11245,7 +11251,7 @@ components:
type: apiKey
info:
title: msgvault API
version: 2.12.0
version: 2.13.0
openapi: 3.1.0
paths:
/api/ping:
Expand Down
12 changes: 12 additions & 0 deletions cmd/msgvault/cmd/daemon_cli_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/kit/daemon"
"go.kenn.io/msgvault/internal/api"
"go.kenn.io/msgvault/internal/apiprotocol"
"go.kenn.io/msgvault/internal/config"
)

Expand Down Expand Up @@ -52,6 +54,7 @@ type daemonCLIDeleteStagedPlanTestRequest struct {
}

type daemonCLIDeduplicatePlanTestRequest struct {
PlanProtocol string `json:"plan_protocol"`
Account string `json:"account"`
Collection string `json:"collection"`
Prefer string `json:"prefer"`
Expand Down Expand Up @@ -233,6 +236,14 @@ func newDaemonCLIDeduplicateTestServer(
Service: daemonService,
Version: Version,
}))
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
if !assert.NoError(t, json.NewEncoder(w).Encode(map[string]any{
"status": "ok", "api_schema_version": api.APISchemaVersion,
})) {
return
}
})
mux.HandleFunc("/api/v1/cli/deduplicate/plan", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method, "plan method")
planRequests.Add(1)
Expand All @@ -242,6 +253,7 @@ func newDaemonCLIDeduplicateTestServer(
http.Error(w, "bad request", http.StatusBadRequest)
return
}
assert.Equal(t, apiprotocol.DeduplicatePlanProtocol, req.PlanProtocol, "plan protocol")
if checkPlan != nil {
checkPlan(req)
}
Expand Down
13 changes: 12 additions & 1 deletion cmd/msgvault/cmd/daemon_cli_subprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (

const daemonCLISubprocessEnv = "MSGVAULT_DAEMON_CLI_PARENT_PID"

var daemonCLIExecutableResolver = os.Executable

func isDaemonCLISubprocess() bool {
return os.Getenv(daemonCLISubprocessEnv) == strconv.Itoa(os.Getppid())
}
Expand Down Expand Up @@ -113,7 +115,7 @@ func classifyDaemonCLIWaitErr(waitErr error, args []string) error {
}

func newDaemonCLISubprocessCommand(ctx context.Context, commandArgs []string, env map[string]string, cwd string) (*exec.Cmd, error) {
exe, err := os.Executable()
exe, err := daemonCLIExecutableResolver()
if err != nil {
return nil, fmt.Errorf("locate msgvault executable: %w", err)
}
Expand Down Expand Up @@ -151,6 +153,9 @@ func daemonCLIChildEnv(base []string, parentPID int, extra map[string]string) []
continue
}
if key, ok := splitEnvEntry(entry); ok {
if strings.EqualFold(key, remoteDeleteEnvVar) {
continue
}
if extraValue, exists := extra[key]; exists {
if !extraReplaced[key] {
out = append(out, key+"="+extraValue)
Expand All @@ -165,6 +170,12 @@ func daemonCLIChildEnv(base []string, parentPID int, extra map[string]string) []
out = append(out, value)
}
for _, key := range sortedEnvKeys(extra) {
if strings.EqualFold(key, remoteDeleteEnvVar) {
if key == remoteDeleteEnvVar {
out = append(out, remoteDeleteEnvVar+"="+extra[key])
}
continue
}
if !extraReplaced[key] {
out = append(out, key+"="+extra[key])
}
Expand Down
34 changes: 34 additions & 0 deletions cmd/msgvault/cmd/daemon_cli_subprocess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"runtime"
"strconv"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -127,6 +128,39 @@ func TestDaemonCLIChildEnvAppliesAllowlistedEnvOverrides(t *testing.T) {
}, got)
}

func TestDaemonCLIChildEnvStripsUnforwardedRemoteDeleteConsent(t *testing.T) {
base := []string{
"PATH=/usr/bin",
"msgvault_enable_remote_delete=1",
"MsgVault_Enable_Remote_Delete=true",
remoteDeleteEnvVar + "=1",
}
tests := []struct {
name string
extra map[string]string
want []string
}{
{name: "no consent"},
{name: "lowercase extra ignored", extra: map[string]string{"msgvault_enable_remote_delete": "1"}},
{name: "mixed case extra ignored", extra: map[string]string{"MsgVault_Enable_Remote_Delete": "1"}},
{name: "canonical consent", extra: map[string]string{remoteDeleteEnvVar: "1"}, want: []string{remoteDeleteEnvVar + "=1"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := daemonCLIChildEnv(base, 123, tt.extra)
var equivalents []string
for _, entry := range got {
key, ok := splitEnvEntry(entry)
if ok && strings.EqualFold(key, remoteDeleteEnvVar) {
equivalents = append(equivalents, entry)
}
}
assert.Equal(t, tt.want, equivalents)
})
}
}

func TestNewDaemonCLISubprocessCommandAppliesWorkingDirectory(t *testing.T) {
cwd := t.TempDir()

Expand Down
Loading