mirror of https://github.com/portainer/portainer
181 lines
5.7 KiB
Go
181 lines
5.7 KiB
Go
package registries
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
portainer "github.com/portainer/portainer/api"
|
|
"github.com/portainer/portainer/pkg/fips"
|
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
|
"github.com/portainer/portainer/pkg/liboras"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
"oras.land/oras-go/v2/registry/remote/errcode"
|
|
)
|
|
|
|
type registryPingPayload struct {
|
|
// Registry Type. Valid values are:
|
|
// 1 (Quay.io),
|
|
// 2 (Azure container registry),
|
|
// 3 (custom registry),
|
|
// 4 (Gitlab registry),
|
|
// 5 (ProGet registry),
|
|
// 6 (DockerHub)
|
|
// 7 (ECR)
|
|
// 8 (Github registry)
|
|
Type portainer.RegistryType `example:"6" validate:"required" enums:"1,2,3,4,5,6,7,8"`
|
|
// URL or IP address of the Docker registry
|
|
URL string `example:"registry-1.docker.io" validate:"required"`
|
|
// Username used to authenticate against this registry
|
|
Username string `example:"registry_user"`
|
|
// Password used to authenticate against this registry
|
|
Password string `example:"registry_password"`
|
|
// Use TLS
|
|
TLS bool `example:"true"`
|
|
}
|
|
|
|
type registryPingResponse struct {
|
|
// Success indicates if the registry connection was successful
|
|
Success bool `json:"success" example:"true"`
|
|
// Message provides details about the connection test result
|
|
Message string `json:"message" example:"Registry connection successful"`
|
|
}
|
|
|
|
func (payload *registryPingPayload) Validate(_ *http.Request) error {
|
|
if len(payload.Username) == 0 || len(payload.Password) == 0 {
|
|
return httperror.BadRequest("Username and password are required", nil)
|
|
}
|
|
|
|
switch payload.Type {
|
|
case portainer.QuayRegistry, portainer.AzureRegistry, portainer.CustomRegistry, portainer.GitlabRegistry, portainer.ProGetRegistry, portainer.DockerHubRegistry, portainer.EcrRegistry, portainer.GithubRegistry:
|
|
default:
|
|
return httperror.BadRequest("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry), 4 (Gitlab registry), 5 (ProGet registry), 6 (DockerHub), 7 (ECR), 8 (Github registry)", nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// @id RegistryPing
|
|
// @summary Test registry connection
|
|
// @description Test connection to a registry with provided credentials
|
|
// @description **Access policy**: authenticated
|
|
// @tags registries
|
|
// @security ApiKeyAuth
|
|
// @security jwt
|
|
// @accept json
|
|
// @produce json
|
|
// @param body body registryPingPayload true "Registry credentials to test"
|
|
// @success 200 {object} registryPingResponse "Success"
|
|
// @failure 400 "Invalid request"
|
|
// @failure 500 "Server error"
|
|
// @router /registries/ping [post]
|
|
func (handler *Handler) pingRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
var payload registryPingPayload
|
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
|
if err != nil {
|
|
return httperror.BadRequest("Invalid request payload", err)
|
|
}
|
|
|
|
// Create a temporary registry configuration for testing
|
|
tempRegistry := &portainer.Registry{
|
|
Type: payload.Type,
|
|
URL: payload.URL,
|
|
Authentication: true,
|
|
Username: payload.Username,
|
|
Password: payload.Password,
|
|
}
|
|
|
|
// For DockerHub, ensure URL is set correctly
|
|
if payload.Type == portainer.DockerHubRegistry && payload.URL == "" {
|
|
tempRegistry.URL = "registry-1.docker.io"
|
|
}
|
|
|
|
// Set up TLS configuration
|
|
if payload.Type == portainer.CustomRegistry {
|
|
tempRegistry.ManagementConfiguration = &portainer.RegistryManagementConfiguration{
|
|
Type: payload.Type,
|
|
TLSConfig: portainer.TLSConfiguration{
|
|
TLS: payload.TLS || fips.FIPSMode(),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Test the registry connection
|
|
success, message := handler.testRegistryConnection(tempRegistry)
|
|
|
|
responseData := registryPingResponse{
|
|
Success: success,
|
|
Message: message,
|
|
}
|
|
|
|
return response.JSON(w, responseData)
|
|
}
|
|
|
|
// testRegistryConnection tests if we can connect to the registry
|
|
func (handler *Handler) testRegistryConnection(registry *portainer.Registry) (bool, string) {
|
|
registryClient, err := liboras.CreateClient(*registry)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("registryURL", registry.URL).Msg("Failed to create registry client")
|
|
return false, "Connection error: Failed to create registry client - " + err.Error()
|
|
}
|
|
|
|
ctx := context.Background()
|
|
err = registryClient.Ping(ctx)
|
|
if err != nil {
|
|
errorMessage := categorizeRegistryError(err, registry.URL)
|
|
return false, errorMessage
|
|
}
|
|
|
|
log.Debug().Str("registryURL", registry.URL).Msg("Registry ping successful")
|
|
return true, "Registry connection successful"
|
|
}
|
|
|
|
// categorizeRegistryError analyzes the error and returns a user-friendly message
|
|
// that distinguishes between connection errors and authentication errors
|
|
func categorizeRegistryError(err error, registryURL string) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
|
|
var userMessage string
|
|
|
|
var errResp *errcode.ErrorResponse
|
|
if errors.As(err, &errResp) {
|
|
|
|
// 401 Unauthorized or 403 Forbidden = authentication/authorization issue
|
|
if errResp.StatusCode == http.StatusUnauthorized || errResp.StatusCode == http.StatusForbidden {
|
|
userMessage = "Access token invalid: Authentication failed - please verify your username and access token"
|
|
} else {
|
|
userMessage = "Connection error: " + err.Error()
|
|
}
|
|
|
|
logEvent := log.Error().
|
|
Err(err).
|
|
Str("registryURL", registryURL).
|
|
Int("statusCode", errResp.StatusCode).
|
|
Str("userMessage", userMessage)
|
|
|
|
if len(errResp.Errors) > 0 {
|
|
logEvent.Interface("errors", errResp.Errors)
|
|
}
|
|
|
|
logEvent.Msg("Registry ping failed")
|
|
|
|
return userMessage
|
|
}
|
|
|
|
// Default: treat everything else as connection error
|
|
userMessage = "Connection error: " + err.Error()
|
|
|
|
log.Error().
|
|
Err(err).
|
|
Str("registryURL", registryURL).
|
|
Str("userMessage", userMessage).
|
|
Msg("Registry ping failed")
|
|
|
|
return userMessage
|
|
}
|