fix: harden DynDNS recovery and publish v1.5.1
This commit is contained in:
1 parent
3383b91e49
commit
81def77e3c
37 files changed
+2415
-250
No files matched your search
@@ -104,6 +104,9 @@ func (r *DockerRunner) Update(ctx context.Context, request updateprotocol.Reques
|
||||
targetReference := r.imageRepository + ":" + request.Version
|
||||
progress(updateprotocol.PhasePulling, "Container-Image wird geladen und geprüft")
|
||||
if err := r.docker.pullImage(ctx, r.imageRepository, request.Version, request.RegistryUsername, request.RegistryToken); err != nil {
|
||||
if errors.Is(err, errRegistryAuthentication) {
|
||||
return &updateError{message: "Container-Image ist nicht öffentlich zugänglich oder der Zugang wurde abgelehnt. Bitte einen gültigen Gitea-Zugang mit Paket-Leserechten hinterlegen; der Dienst läuft unverändert weiter", err: err}
|
||||
}
|
||||
return &updateError{message: "Image konnte nicht geladen werden; der Dienst läuft unverändert weiter", err: err}
|
||||
}
|
||||
targetImage, err := r.docker.inspectImage(ctx, targetReference)
|
||||
@@ -137,10 +140,12 @@ func (r *DockerRunner) Update(ctx context.Context, request updateprotocol.Reques
|
||||
}()
|
||||
|
||||
progress(updateprotocol.PhaseRestarting, "Anwendung wird kontrolliert neu gestartet")
|
||||
// A lost Docker response does not mean the stop was not executed. Arm
|
||||
// rollback first so a stopped application is restarted on this error path.
|
||||
oldStopped = true
|
||||
if err := r.docker.stopContainer(ctx, target.ID, 20); err != nil {
|
||||
return err
|
||||
}
|
||||
oldStopped = true
|
||||
if err := r.docker.renameContainer(ctx, target.ID, backupName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -315,9 +320,23 @@ func (c *dockerClient) findTarget(ctx context.Context, label string) (containerS
|
||||
return running[0], nil
|
||||
}
|
||||
|
||||
var errRegistryAuthentication = errors.New("registry authentication required")
|
||||
|
||||
func (c *dockerClient) pullImage(ctx context.Context, repository, tag, username, credential string) error {
|
||||
authJSON, _ := json.Marshal(map[string]string{"username": username, "password": credential, "serveraddress": strings.Split(repository, "/")[0]})
|
||||
headers := http.Header{"X-Registry-Auth": {base64.URLEncoding.EncodeToString(authJSON)}}
|
||||
// Public packages must work without credentials, including stale saved ones.
|
||||
err := c.pullImageOnce(ctx, repository, tag, "", "")
|
||||
if errors.Is(err, errRegistryAuthentication) && username != "" && credential != "" {
|
||||
return c.pullImageOnce(ctx, repository, tag, username, credential)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *dockerClient) pullImageOnce(ctx context.Context, repository, tag, username, credential string) error {
|
||||
var headers http.Header
|
||||
if username != "" && credential != "" {
|
||||
authJSON, _ := json.Marshal(map[string]string{"username": username, "password": credential, "serveraddress": strings.Split(repository, "/")[0]})
|
||||
headers = http.Header{"X-Registry-Auth": {base64.URLEncoding.EncodeToString(authJSON)}}
|
||||
}
|
||||
query := url.Values{"fromImage": {repository}, "tag": {tag}}
|
||||
response, err := c.request(ctx, http.MethodPost, "/images/create?"+query.Encode(), nil, headers, true)
|
||||
if err != nil {
|
||||
@@ -325,21 +344,49 @@ func (c *dockerClient) pullImage(ctx context.Context, repository, tag, username,
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return dockerStatusError(response)
|
||||
err := dockerStatusError(response)
|
||||
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden || registryAuthFailure(err.Error()) {
|
||||
return fmt.Errorf("%w: %v", errRegistryAuthentication, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
scanner.Buffer(make([]byte, 64<<10), 1<<20)
|
||||
for scanner.Scan() {
|
||||
var event struct {
|
||||
Error string `json:"error"`
|
||||
Error string `json:"error"`
|
||||
ErrorDetail struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"errorDetail"`
|
||||
}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &event); err == nil && event.Error != "" {
|
||||
return errors.New(event.Error)
|
||||
if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
|
||||
return errors.New("Docker returned invalid pull progress")
|
||||
}
|
||||
message := event.Error
|
||||
if message == "" {
|
||||
message = event.ErrorDetail.Message
|
||||
}
|
||||
if message != "" {
|
||||
if event.ErrorDetail.Code == http.StatusUnauthorized || event.ErrorDetail.Code == http.StatusForbidden || registryAuthFailure(message) {
|
||||
return fmt.Errorf("%w: %s", errRegistryAuthentication, message)
|
||||
}
|
||||
return errors.New(message)
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func registryAuthFailure(message string) bool {
|
||||
message = strings.ToLower(message)
|
||||
for _, marker := range []string{"unauthorized", "authentication required", "access denied", "access to the resource is denied", "pull access denied", "no basic auth credentials"} {
|
||||
if strings.Contains(message, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *dockerClient) inspectContainer(ctx context.Context, id string) (containerInspect, error) {
|
||||
response, err := c.request(ctx, http.MethodGet, "/containers/"+url.PathEscape(id)+"/json", nil, nil, true)
|
||||
if err != nil {
|
||||
@@ -805,9 +852,9 @@ func (c *dockerClient) removeContainer(ctx context.Context, id string, force boo
|
||||
}
|
||||
|
||||
func (c *dockerClient) waitHealthy(ctx context.Context, id, expectedImageID, expectedVersion string, timeout time.Duration) error {
|
||||
deadline := time.NewTimer(timeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer deadline.Stop()
|
||||
defer ticker.Stop()
|
||||
consecutiveHealthy := 0
|
||||
startedAt := time.Now()
|
||||
@@ -815,8 +862,6 @@ func (c *dockerClient) waitHealthy(ctx context.Context, id, expectedImageID, exp
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-deadline.C:
|
||||
return errors.New("replacement container did not become healthy in time")
|
||||
case <-ticker.C:
|
||||
inspected, err := c.inspectContainer(ctx, id)
|
||||
if err != nil {
|
||||
@@ -859,16 +904,14 @@ func (c *dockerClient) waitHealthy(ctx context.Context, id, expectedImageID, exp
|
||||
}
|
||||
|
||||
func (c *dockerClient) waitCurrentHealthy(ctx context.Context, id, expectedImageID string, timeout, pollInterval time.Duration) (containerInspect, error) {
|
||||
deadline := time.NewTimer(timeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
defer deadline.Stop()
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return containerInspect{}, ctx.Err()
|
||||
case <-deadline.C:
|
||||
return containerInspect{}, errors.New("current container did not become healthy in time")
|
||||
case <-ticker.C:
|
||||
inspected, err := c.inspectContainer(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user