feat: add resilient updates and access controls
Container-Image veröffentlichen / container (push) Has been cancelled
Container-Image veröffentlichen / container (push) Has been cancelled
This commit is contained in:
1 parent
a000152423
commit
f02ec9977c
41 files changed
+3277
-151
No files matched your search
@@ -0,0 +1,95 @@
|
||||
package updateagent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/updateprotocol"
|
||||
)
|
||||
|
||||
func TestRedactSecretRemovesCredentialFromErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
const secret = "sensitive-registry-token"
|
||||
redacted := redactSecret(errors.New("registry rejected "+secret), secret)
|
||||
if strings.Contains(redacted.Error(), secret) || !strings.Contains(redacted.Error(), "[redacted]") {
|
||||
t.Fatalf("credential was not redacted: %q", redacted)
|
||||
}
|
||||
}
|
||||
|
||||
type blockingRunner struct {
|
||||
started chan updateprotocol.Request
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (r *blockingRunner) Update(ctx context.Context, request updateprotocol.Request, progress func(phase, message string)) error {
|
||||
progress(updateprotocol.PhasePulling, "image is being pulled")
|
||||
r.started <- request
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-r.release:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestControllerValidatesRequestsAndRunsOnlyOneUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
runner := &blockingRunner{started: make(chan updateprotocol.Request, 1), release: make(chan struct{})}
|
||||
controller := NewController(runner, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
handler := controller.Handler()
|
||||
|
||||
invalid := httptest.NewRecorder()
|
||||
handler.ServeHTTP(invalid, httptest.NewRequest(http.MethodPost, "/v1/update", strings.NewReader(`{"version":"stable","registry_username":"user","registry_token":"secret-token"}`)))
|
||||
if invalid.Code != http.StatusBadRequest {
|
||||
t.Fatalf("invalid version accepted: status=%d", invalid.Code)
|
||||
}
|
||||
trailing := httptest.NewRecorder()
|
||||
handler.ServeHTTP(trailing, httptest.NewRequest(http.MethodPost, "/v1/update", strings.NewReader(`{"version":"v1.2.3","registry_username":"user","registry_token":"secret-token"} {}`)))
|
||||
if trailing.Code != http.StatusBadRequest {
|
||||
t.Fatalf("request with trailing JSON accepted: status=%d", trailing.Code)
|
||||
}
|
||||
|
||||
const secret = "registry-token-must-not-leak"
|
||||
body := []byte(`{"version":"v1.2.3","registry_username":"user","registry_token":"` + secret + `"}`)
|
||||
accepted := httptest.NewRecorder()
|
||||
handler.ServeHTTP(accepted, httptest.NewRequest(http.MethodPost, "/v1/update", bytes.NewReader(body)))
|
||||
if accepted.Code != http.StatusAccepted {
|
||||
t.Fatalf("valid update rejected: status=%d body=%s", accepted.Code, accepted.Body.String())
|
||||
}
|
||||
select {
|
||||
case request := <-runner.started:
|
||||
if request.Version != "v1.2.3" || request.RegistryToken != secret {
|
||||
t.Fatalf("runner received an altered request: %#v", request)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("update runner did not start")
|
||||
}
|
||||
|
||||
busy := httptest.NewRecorder()
|
||||
handler.ServeHTTP(busy, httptest.NewRequest(http.MethodPost, "/v1/update", bytes.NewReader(body)))
|
||||
if busy.Code != http.StatusConflict {
|
||||
t.Fatalf("parallel update was not rejected: status=%d", busy.Code)
|
||||
}
|
||||
status := httptest.NewRecorder()
|
||||
handler.ServeHTTP(status, httptest.NewRequest(http.MethodGet, "/v1/status", nil))
|
||||
if status.Code != http.StatusOK || strings.Contains(status.Body.String(), secret) || !strings.Contains(status.Body.String(), `"phase":"pulling"`) {
|
||||
t.Fatalf("unsafe or invalid public status: status=%d body=%s", status.Code, status.Body.String())
|
||||
}
|
||||
|
||||
close(runner.release)
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for controller.Status().Phase != updateprotocol.PhaseComplete && time.Now().Before(deadline) {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if controller.Status().Phase != updateprotocol.PhaseComplete {
|
||||
t.Fatalf("update did not complete: %#v", controller.Status())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user