fix: stabilize software updates
This commit is contained in:
1 parent
fc59092030
commit
26a960c6bd
25 files changed
+487
-146
No files matched your search
@@ -30,6 +30,7 @@ type State struct {
|
||||
LastCheckedAt time.Time
|
||||
LastError string
|
||||
CredentialConfigured bool
|
||||
CredentialUsername string
|
||||
UpdateAvailable bool
|
||||
AgentAvailable bool
|
||||
AgentCompatible bool
|
||||
@@ -136,15 +137,15 @@ func (m *Manager) State() State {
|
||||
return m.state
|
||||
}
|
||||
|
||||
func (m *Manager) SaveCredential(ctx context.Context, username, token string) error {
|
||||
username, token = strings.TrimSpace(username), strings.TrimSpace(token)
|
||||
func (m *Manager) SaveCredential(ctx context.Context, username, credential string) error {
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" || len(username) > 128 {
|
||||
return errors.New("Gitea-Benutzername fehlt oder ist zu lang")
|
||||
}
|
||||
if len(token) < 8 || len(token) > 1024 {
|
||||
return errors.New("Gitea-Token fehlt oder hat eine ungültige Länge")
|
||||
if len(credential) < 8 || len(credential) > 1024 {
|
||||
return errors.New("Gitea-Passwort oder -Token fehlt oder hat eine ungültige Länge")
|
||||
}
|
||||
ciphertext, nonce, err := m.cipher.Encrypt(token)
|
||||
ciphertext, nonce, err := m.cipher.Encrypt(credential)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Update-Zugang verschlüsseln: %w", err)
|
||||
}
|
||||
@@ -169,11 +170,11 @@ func (m *Manager) CheckNow(ctx context.Context) error {
|
||||
m.setStoredState(settings)
|
||||
return ErrCredentialMissing
|
||||
}
|
||||
token, err := m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
credential, err := m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
if err != nil {
|
||||
return m.recordFailure(ctx, errors.New("gespeicherter Update-Zugang konnte nicht entschlüsselt werden"))
|
||||
}
|
||||
release, err := m.fetchLatest(ctx, token)
|
||||
release, err := m.fetchLatest(ctx, settings.RegistryUsername, credential)
|
||||
if err != nil {
|
||||
return m.recordFailure(ctx, err)
|
||||
}
|
||||
@@ -207,14 +208,14 @@ func (m *Manager) RequestUpdate(ctx context.Context, version string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token, err := m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
credential, err := m.cipher.Decrypt(settings.TokenCiphertext, settings.TokenNonce)
|
||||
if err != nil || settings.RegistryUsername == "" {
|
||||
return ErrCredentialMissing
|
||||
}
|
||||
if err := m.store.SetDesiredSoftwareVersion(ctx, version); err != nil {
|
||||
return err
|
||||
}
|
||||
request := updateprotocol.Request{Version: version, CurrentVersion: state.CurrentVersion, RegistryUsername: settings.RegistryUsername, RegistryToken: token}
|
||||
request := updateprotocol.Request{Version: version, CurrentVersion: state.CurrentVersion, RegistryUsername: settings.RegistryUsername, RegistryToken: credential}
|
||||
if err := m.agent.Start(ctx, request); err != nil {
|
||||
cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
m.clearDesiredVersion(cleanupCtx, version)
|
||||
@@ -254,17 +255,17 @@ func (m *Manager) UpdateStatus(ctx context.Context) (updateprotocol.Status, erro
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (m *Manager) fetchLatest(ctx context.Context, token string) (releaseResponse, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, m.apiURL, nil)
|
||||
func (m *Manager) fetchLatest(ctx context.Context, username, credential string) (releaseResponse, error) {
|
||||
response, err := m.fetchLatestWithAuth(ctx, username, credential, true)
|
||||
if err != nil {
|
||||
return releaseResponse{}, errors.New("Update-Endpunkt ist ungültig")
|
||||
return releaseResponse{}, err
|
||||
}
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("Authorization", "token "+token)
|
||||
request.Header.Set("User-Agent", "domain-control-update-checker/"+buildinfo.Version)
|
||||
response, err := m.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return releaseResponse{}, errors.New("Git-Server ist momentan nicht erreichbar")
|
||||
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
|
||||
response.Body.Close()
|
||||
response, err = m.fetchLatestWithAuth(ctx, username, credential, false)
|
||||
if err != nil {
|
||||
return releaseResponse{}, err
|
||||
}
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
|
||||
@@ -284,6 +285,25 @@ func (m *Manager) fetchLatest(ctx context.Context, token string) (releaseRespons
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (m *Manager) fetchLatestWithAuth(ctx context.Context, username, credential string, basic bool) (*http.Response, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, m.apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("Update-Endpunkt ist ungültig")
|
||||
}
|
||||
request.Header.Set("Accept", "application/json")
|
||||
if basic {
|
||||
request.SetBasicAuth(username, credential)
|
||||
} else {
|
||||
request.Header.Set("Authorization", "token "+credential)
|
||||
}
|
||||
request.Header.Set("User-Agent", "domain-control-update-checker/"+buildinfo.Version)
|
||||
response, err := m.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, errors.New("Git-Server ist momentan nicht erreichbar")
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (m *Manager) safeReleaseURL(raw string) string {
|
||||
apiURL, apiErr := url.Parse(m.apiURL)
|
||||
releaseURL, releaseErr := url.Parse(raw)
|
||||
@@ -313,6 +333,7 @@ func (m *Manager) setStoredState(settings model.SoftwareUpdateSettings) {
|
||||
LastCheckedAt: settings.LastCheckedAt,
|
||||
LastError: settings.LastError,
|
||||
CredentialConfigured: settings.RegistryUsername != "" && len(settings.TokenCiphertext) > 0 && len(settings.TokenNonce) > 0,
|
||||
CredentialUsername: settings.RegistryUsername,
|
||||
}
|
||||
if comparison, err := compareVersions(state.CurrentVersion, state.LatestVersion); err == nil {
|
||||
state.UpdateAvailable = comparison < 0
|
||||
|
||||
@@ -21,10 +21,11 @@ import (
|
||||
|
||||
func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
const token = "test-token-that-must-stay-secret"
|
||||
const credential = " test-password-or-token-that-must-stay-secret "
|
||||
var unavailable atomic.Bool
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "token "+token {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok || username != "registry-user" || password != credential {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -33,7 +34,7 @@ func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"tag_name":"v1.4.2","html_url":"https://example.invalid/releases/v1.4.2","draft":false,"prerelease":false}`))
|
||||
_, _ = w.Write([]byte(`{"tag_name":"v1.4.3","html_url":"https://example.invalid/releases/v1.4.3","draft":false,"prerelease":false}`))
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
@@ -59,22 +60,22 @@ func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manager.SaveCredential(ctx, "registry-user", token); err != nil {
|
||||
if err := manager.SaveCredential(ctx, "registry-user", credential); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manager.CheckNow(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := manager.State()
|
||||
if !state.UpdateAvailable || state.LatestVersion != "v1.4.2" || state.LastError != "" {
|
||||
if !state.UpdateAvailable || state.LatestVersion != "v1.4.3" || state.LastError != "" || state.CredentialUsername != "registry-user" {
|
||||
t.Fatalf("new release was not detected: %#v", state)
|
||||
}
|
||||
settings, err := database.GetSoftwareUpdateSettings(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(settings.TokenCiphertext), token) {
|
||||
t.Fatal("registry token was stored as plaintext")
|
||||
if strings.Contains(string(settings.TokenCiphertext), credential) {
|
||||
t.Fatal("registry credential was stored as plaintext")
|
||||
}
|
||||
|
||||
unavailable.Store(true)
|
||||
@@ -82,11 +83,36 @@ func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
t.Fatal("unavailable Gitea endpoint was reported as successful")
|
||||
}
|
||||
state = manager.State()
|
||||
if !state.UpdateAvailable || state.LatestVersion != "v1.4.2" || state.LastError == "" {
|
||||
if !state.UpdateAvailable || state.LatestVersion != "v1.4.3" || state.LastError == "" {
|
||||
t.Fatalf("temporary outage removed the last known update reminder: %#v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerFallsBackToTokenAuthentication(t *testing.T) {
|
||||
t.Parallel()
|
||||
const credential = "fallback-api-token"
|
||||
var requests atomic.Int32
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests.Add(1)
|
||||
if r.Header.Get("Authorization") != "token "+credential {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"tag_name":"v2.0.0","html_url":"https://example.invalid/releases/v2.0.0","draft":false,"prerelease":false}`))
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
manager := &Manager{apiURL: api.URL, httpClient: api.Client()}
|
||||
release, err := manager.fetchLatest(context.Background(), "registry-user", credential)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if release.TagName != "v2.0.0" || requests.Load() != 2 {
|
||||
t.Fatalf("token fallback was not used: release=%#v requests=%d", release, requests.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentCompatibilityStartsAtStableV14Protocol(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
|
||||
Reference in new issue
Block a user