fix(worker): harden control-plane lifecycle
Все проверки выполнены успешно
CI / test (push) Successful in 2m32s
Docker / Build and publish worker image (push) Successful in 18m17s
Все проверки выполнены успешно
CI / test (push) Successful in 2m32s
Docker / Build and publish worker image (push) Successful in 18m17s
- reconnect safely after token rotation and retry leased results - reject malformed tasks and remove production cluster debug mutation - validate environment files and require immutable container images BREAKING CHANGE: Docker install, deploy, and Compose now require an immutable repository@sha256 image reference.
Этот коммит содержится в:
@@ -1,16 +1,11 @@
|
||||
package webapp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -21,23 +16,13 @@ import (
|
||||
// pinned without standing up a real raft group.
|
||||
type stubCluster struct {
|
||||
stats ClusterStats
|
||||
applyIndex uint64
|
||||
applyErr error
|
||||
applyCalled int
|
||||
applyMu sync.Mutex
|
||||
clusterIDOut string
|
||||
addrOut string
|
||||
}
|
||||
|
||||
func (s *stubCluster) Stats() ClusterStats { return s.stats }
|
||||
func (s *stubCluster) ApplyTestConfig() (uint64, error) {
|
||||
s.applyMu.Lock()
|
||||
defer s.applyMu.Unlock()
|
||||
s.applyCalled++
|
||||
return s.applyIndex, s.applyErr
|
||||
}
|
||||
func (s *stubCluster) ClusterID() string { return s.clusterIDOut }
|
||||
func (s *stubCluster) LocalAddr() string { return s.addrOut }
|
||||
func (s *stubCluster) ClusterID() string { return s.clusterIDOut }
|
||||
func (s *stubCluster) LocalAddr() string { return s.addrOut }
|
||||
|
||||
// withClusterServer returns a test server whose ClusterView is the
|
||||
// supplied stub. The first-run password path is also exercised so
|
||||
@@ -166,151 +151,23 @@ func TestClusterStatus_RequiresSession(t *testing.T) {
|
||||
assert.Equal(t, "/web/login", resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
// TestClusterApplyTestConfig_NotConfigured verifies the 404 path
|
||||
// when WORKER_CLUSTER_DEBUG_APPLY is false (the production default)
|
||||
// and no cluster is attached. The handler must refuse before it
|
||||
// even checks the cluster because the debug flag is off.
|
||||
func TestClusterApplyTestConfig_NotConfigured(t *testing.T) {
|
||||
srv := newTestServer(t, &stubRunner{id: "w-1"})
|
||||
require.False(t, srv.cfg.DebugClusterApply, "default config must leave the debug apply flag off")
|
||||
ts := newHTTPTestServer(t, srv)
|
||||
c, _ := loginAsFirstRun(t, ts.URL, srv)
|
||||
clearRequiresChange(t, srv)
|
||||
// TestClusterApplyTestConfig_NotExposed verifies production requests cannot
|
||||
// append a hardcoded config through the former debug endpoint.
|
||||
func TestClusterApplyTestConfig_NotExposed(t *testing.T) {
|
||||
t.Setenv("WORKER_CLUSTER_DEBUG_APPLY", "true")
|
||||
ts, _, c := withClusterServer(t, &stubCluster{})
|
||||
|
||||
resp, err := c.PostForm(ts.URL+"/web/api/cluster/apply-test-config", url.Values{})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode,
|
||||
"debug apply must be invisible (404) when WORKER_CLUSTER_DEBUG_APPLY is unset")
|
||||
}
|
||||
|
||||
// TestClusterApplyTestConfig_DebugOffReturns404 verifies that even
|
||||
// with a cluster attached the apply endpoint stays 404 unless the
|
||||
// debug flag is on. The flag, not cluster presence, gates the
|
||||
// endpoint.
|
||||
func TestClusterApplyTestConfig_DebugOffReturns404(t *testing.T) {
|
||||
stub := &stubCluster{applyIndex: 42}
|
||||
ts, _, c := withClusterServer(t, stub)
|
||||
|
||||
resp, err := c.Get(ts.URL + "/overview")
|
||||
require.NoError(t, err)
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close() //nolint:errcheck
|
||||
csrf := extractCSRFToken(t, string(bodyBytes))
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("csrf_token", csrf)
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/web/api/cluster/apply-test-config",
|
||||
strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err = c.Do(req)
|
||||
resp, err := c.Post(ts.URL+"/web/api/cluster/apply-test-config", "", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
assert.Equal(t, 0, stub.applyCalled,
|
||||
"ApplyTestConfig must never be called when the debug flag is off")
|
||||
}
|
||||
|
||||
// TestClusterApplyTestConfig_HappyPath verifies that the apply-test-
|
||||
// config endpoint returns the applied index when the cluster
|
||||
// subsystem accepts the entry. CSRF is checked. The DebugClusterApply
|
||||
// flag must be on for the endpoint to be reachable.
|
||||
func TestClusterApplyTestConfig_HappyPath(t *testing.T) {
|
||||
stub := &stubCluster{
|
||||
stats: ClusterStats{
|
||||
NodeID: "worker1", State: "Leader", Leader: "worker1",
|
||||
Voters: []string{"worker1"},
|
||||
},
|
||||
applyIndex: 13,
|
||||
clusterIDOut: "worker1",
|
||||
addrOut: "127.0.0.1:17401",
|
||||
}
|
||||
ts, srv, c := withClusterServer(t, stub)
|
||||
srv.cfg.DebugClusterApply = true
|
||||
|
||||
// Fetch CSRF token from any authenticated page.
|
||||
resp, err := c.Get(ts.URL + "/overview")
|
||||
require.NoError(t, err)
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close() //nolint:errcheck
|
||||
csrf := extractCSRFToken(t, string(bodyBytes))
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("csrf_token", csrf)
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/web/api/cluster/apply-test-config",
|
||||
strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err = c.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
bodyBytes, _ = io.ReadAll(resp.Body)
|
||||
|
||||
var got map[string]uint64
|
||||
require.NoError(t, json.Unmarshal(bodyBytes, &got))
|
||||
assert.EqualValues(t, 13, got["applied_index"])
|
||||
|
||||
assert.Equal(t, 1, stub.applyCalled)
|
||||
}
|
||||
|
||||
// TestClusterApplyTestConfig_PropagatesError verifies that errors
|
||||
// from the cluster subsystem surface as 502 Bad Gateway. Debug flag
|
||||
// must be on.
|
||||
func TestClusterApplyTestConfig_PropagatesError(t *testing.T) {
|
||||
stub := &stubCluster{
|
||||
applyErr: errStubApply,
|
||||
clusterIDOut: "worker1",
|
||||
addrOut: "127.0.0.1:17401",
|
||||
}
|
||||
ts, srv, c := withClusterServer(t, stub)
|
||||
srv.cfg.DebugClusterApply = true
|
||||
|
||||
resp, err := c.Get(ts.URL + "/overview")
|
||||
require.NoError(t, err)
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close() //nolint:errcheck
|
||||
csrf := extractCSRFToken(t, string(bodyBytes))
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("csrf_token", csrf)
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/web/api/cluster/apply-test-config",
|
||||
strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err = c.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
|
||||
}
|
||||
|
||||
// TestClusterApplyTestConfig_RequiresCSRF ensures the apply-test-
|
||||
// config POST is refused without a CSRF token. Debug flag must be
|
||||
// on for the endpoint to be reachable; without the flag it returns
|
||||
// 404 (priority over CSRF check).
|
||||
func TestClusterApplyTestConfig_RequiresCSRF(t *testing.T) {
|
||||
stub := &stubCluster{applyIndex: 99}
|
||||
ts, srv, c := withClusterServer(t, stub)
|
||||
srv.cfg.DebugClusterApply = true
|
||||
|
||||
resp, err := c.PostForm(ts.URL+"/web/api/cluster/apply-test-config", url.Values{})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode,
|
||||
"apply-test-config without CSRF must be 403")
|
||||
assert.Equal(t, 0, stub.applyCalled, "ApplyTestConfig must not be called without CSRF")
|
||||
}
|
||||
|
||||
// errStubApply is a sentinel error used by the apply-error test.
|
||||
var errStubApply = errApply("worker not leader")
|
||||
|
||||
type errApply string
|
||||
|
||||
func (e errApply) Error() string { return string(e) }
|
||||
|
||||
// TestSetClusterDetaches verifies SetCluster(nil) returns the server
|
||||
// to the no-cluster-attached state (503 from the endpoints).
|
||||
func TestSetClusterDetaches(t *testing.T) {
|
||||
srv := newTestServer(t, &stubRunner{id: "w-1"})
|
||||
stub := &stubCluster{applyIndex: 7}
|
||||
stub := &stubCluster{}
|
||||
srv.SetCluster(stub)
|
||||
require.NotNil(t, srv.Cluster())
|
||||
|
||||
@@ -326,11 +183,3 @@ func TestSetClusterDetaches(t *testing.T) {
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
|
||||
}
|
||||
|
||||
// _ = context.Background and time.Time keep the linter quiet about
|
||||
// unused imports if the file shrinks.
|
||||
var (
|
||||
_ = context.Background
|
||||
_ = time.Now
|
||||
_ = url.Parse
|
||||
)
|
||||
|
||||
Ссылка в новой задаче
Block a user