refactor(api): restructured API for type safety, maintainability and docs generation

- These changes makes the API incombatible with previous versions
- Added new types for error handling, success responses, and health checks.
- Updated health check logic to utilize the new types for better clarity and structure.
- Refactored existing handlers to improve response consistency and error handling.
- Updated Makefile to include a new target for generating API types from Swagger.
- Updated "new agent" API to respond an encrypted cert pair
This commit is contained in:
yusing
2025-08-16 13:04:05 +08:00
parent fce9ce21c9
commit 35a3e3fef6
149 changed files with 13173 additions and 2173 deletions

View File

@@ -13,57 +13,19 @@ import (
"github.com/docker/go-connections/nat"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/gperr"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
"github.com/yusing/go-proxy/internal/serialization"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/utils"
)
type (
PortMapping = map[int]container.Port
Container struct {
_ utils.NoCopy
DockerHost string `json:"docker_host"`
Image *ContainerImage `json:"image"`
ContainerName string `json:"container_name"`
ContainerID string `json:"container_id"`
Agent *agent.AgentConfig `json:"agent"`
Labels map[string]string `json:"-"`
IdlewatcherConfig *idlewatcher.Config `json:"idlewatcher_config"`
Mounts []string `json:"mounts"`
Network string `json:"network,omitempty"`
PublicPortMapping PortMapping `json:"public_ports"` // non-zero publicPort:types.Port
PrivatePortMapping PortMapping `json:"private_ports"` // privatePort:types.Port
PublicHostname string `json:"public_hostname"`
PrivateHostname string `json:"private_hostname"`
Aliases []string `json:"aliases"`
IsExcluded bool `json:"is_excluded"`
IsExplicit bool `json:"is_explicit"`
IsHostNetworkMode bool `json:"is_host_network_mode"`
Running bool `json:"running"`
Errors *containerError `json:"errors"`
}
ContainerImage struct {
Author string `json:"author,omitempty"`
Name string `json:"name"`
Tag string `json:"tag,omitempty"`
}
)
var DummyContainer = new(Container)
var DummyContainer = new(types.Container)
var (
ErrNetworkNotFound = errors.New("network not found")
ErrNoNetwork = errors.New("no network found")
)
func FromDocker(c *container.SummaryTrimmed, dockerHost string) (res *Container) {
func FromDocker(c *container.Summary, dockerHost string) (res *types.Container) {
_, isExplicit := c.Labels[LabelAliases]
helper := containerHelper{c}
if !isExplicit {
@@ -78,7 +40,7 @@ func FromDocker(c *container.SummaryTrimmed, dockerHost string) (res *Container)
network := helper.getDeleteLabel(LabelNetwork)
isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude))
res = &Container{
res = &types.Container{
DockerHost: dockerHost,
Image: helper.parseImage(),
ContainerName: helper.getName(),
@@ -103,25 +65,25 @@ func FromDocker(c *container.SummaryTrimmed, dockerHost string) (res *Container)
var ok bool
res.Agent, ok = agent.GetAgent(dockerHost)
if !ok {
res.addError(fmt.Errorf("agent %q not found", dockerHost))
addError(res, fmt.Errorf("agent %q not found", dockerHost))
}
}
res.setPrivateHostname(helper)
res.setPublicHostname()
res.loadDeleteIdlewatcherLabels(helper)
setPrivateHostname(res, helper)
setPublicHostname(res)
loadDeleteIdlewatcherLabels(res, helper)
if res.PrivateHostname == "" && res.PublicHostname == "" && res.Running {
res.addError(ErrNoNetwork)
addError(res, ErrNoNetwork)
}
return
}
func (c *Container) IsBlacklisted() bool {
return c.Image.IsBlacklisted() || c.isDatabase()
func IsBlacklisted(c *types.Container) bool {
return IsBlacklistedImage(c.Image) || isDatabase(c)
}
func (c *Container) UpdatePorts() error {
func UpdatePorts(c *types.Container) error {
client, err := NewClient(c.DockerHost)
if err != nil {
return err
@@ -148,15 +110,15 @@ func (c *Container) UpdatePorts() error {
return nil
}
func (c *Container) DockerComposeProject() string {
func DockerComposeProject(c *types.Container) string {
return c.Labels["com.docker.compose.project"]
}
func (c *Container) DockerComposeService() string {
func DockerComposeService(c *types.Container) string {
return c.Labels["com.docker.compose.service"]
}
func (c *Container) Dependencies() []string {
func Dependencies(c *types.Container) []string {
deps := c.Labels[LabelDependsOn]
if deps == "" {
deps = c.Labels["com.docker.compose.depends_on"]
@@ -173,7 +135,7 @@ var databaseMPs = map[string]struct{}{
"/var/lib/rabbitmq": {},
}
func (c *Container) isDatabase() bool {
func isDatabase(c *types.Container) bool {
for _, m := range c.Mounts {
if _, ok := databaseMPs[m]; ok {
return true
@@ -190,7 +152,7 @@ func (c *Container) isDatabase() bool {
return false
}
func (c *Container) isLocal() bool {
func isLocal(c *types.Container) bool {
if strings.HasPrefix(c.DockerHost, "unix://") {
return true
}
@@ -206,11 +168,11 @@ func (c *Container) isLocal() bool {
return hostname == "localhost"
}
func (c *Container) setPublicHostname() {
func setPublicHostname(c *types.Container) {
if !c.Running {
return
}
if c.isLocal() {
if isLocal(c) {
c.PublicHostname = "127.0.0.1"
return
}
@@ -222,8 +184,8 @@ func (c *Container) setPublicHostname() {
c.PublicHostname = url.Hostname()
}
func (c *Container) setPrivateHostname(helper containerHelper) {
if !c.isLocal() && c.Agent == nil {
func setPrivateHostname(c *types.Container, helper containerHelper) {
if !isLocal(c) && c.Agent == nil {
return
}
if helper.NetworkSettings == nil {
@@ -236,7 +198,7 @@ func (c *Container) setPrivateHostname(helper containerHelper) {
return
}
// try {project_name}_{network_name}
if proj := c.DockerComposeProject(); proj != "" {
if proj := DockerComposeProject(c); proj != "" {
oldNetwork, newNetwork := c.Network, fmt.Sprintf("%s_%s", proj, c.Network)
if newNetwork != oldNetwork {
v, ok = helper.NetworkSettings.Networks[newNetwork]
@@ -248,7 +210,7 @@ func (c *Container) setPrivateHostname(helper containerHelper) {
}
}
nearest := gperr.DoYouMean(utils.NearestField(c.Network, helper.NetworkSettings.Networks))
c.addError(fmt.Errorf("network %q not found, %w", c.Network, nearest))
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
return
}
// fallback to first network if no network is specified
@@ -261,7 +223,7 @@ func (c *Container) setPrivateHostname(helper containerHelper) {
}
}
func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
func loadDeleteIdlewatcherLabels(c *types.Container, helper containerHelper) {
cfg := map[string]any{
"idle_timeout": helper.getDeleteLabel(LabelIdleTimeout),
"wake_timeout": helper.getDeleteLabel(LabelWakeTimeout),
@@ -269,7 +231,7 @@ func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
"stop_timeout": helper.getDeleteLabel(LabelStopTimeout),
"stop_signal": helper.getDeleteLabel(LabelStopSignal),
"start_endpoint": helper.getDeleteLabel(LabelStartEndpoint),
"depends_on": c.Dependencies(),
"depends_on": Dependencies(c),
}
// ensure it's deleted from labels
@@ -278,8 +240,8 @@ func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
// set only if idlewatcher is enabled
idleTimeout := cfg["idle_timeout"]
if idleTimeout != "" {
idwCfg := new(idlewatcher.Config)
idwCfg.Docker = &idlewatcher.DockerConfig{
idwCfg := new(types.IdlewatcherConfig)
idwCfg.Docker = &types.DockerConfig{
DockerHost: c.DockerHost,
ContainerID: c.ContainerID,
ContainerName: c.ContainerName,
@@ -287,16 +249,16 @@ func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
err := serialization.MapUnmarshalValidate(cfg, idwCfg)
if err != nil {
c.addError(err)
addError(c, err)
} else {
c.IdlewatcherConfig = idwCfg
}
}
}
func (c *Container) addError(err error) {
func addError(c *types.Container, err error) {
if c.Errors == nil {
c.Errors = new(containerError)
c.Errors = new(types.ContainerError)
}
c.Errors.Add(err)
}

View File

@@ -4,11 +4,12 @@ import (
"strings"
"github.com/docker/docker/api/types/container"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type containerHelper struct {
*container.SummaryTrimmed
*container.Summary
}
// getDeleteLabel gets the value of a label and then deletes it from the container.
@@ -40,10 +41,10 @@ func (c containerHelper) getMounts() []string {
return m
}
func (c containerHelper) parseImage() *ContainerImage {
func (c containerHelper) parseImage() *types.ContainerImage {
colonSep := strutils.SplitRune(c.Image, ':')
slashSep := strutils.SplitRune(colonSep[0], '/')
im := new(ContainerImage)
im := new(types.ContainerImage)
if len(slashSep) > 1 {
im.Author = strings.Join(slashSep[:len(slashSep)-1], "/")
im.Name = slashSep[len(slashSep)-1]
@@ -59,8 +60,8 @@ func (c containerHelper) parseImage() *ContainerImage {
return im
}
func (c containerHelper) getPublicPortMapping() PortMapping {
res := make(PortMapping)
func (c containerHelper) getPublicPortMapping() types.PortMapping {
res := make(types.PortMapping)
for _, v := range c.Ports {
if v.PublicPort == 0 {
continue
@@ -70,8 +71,8 @@ func (c containerHelper) getPublicPortMapping() PortMapping {
return res
}
func (c containerHelper) getPrivatePortMapping() PortMapping {
res := make(PortMapping)
func (c containerHelper) getPrivatePortMapping() types.PortMapping {
res := make(types.PortMapping)
for _, v := range c.Ports {
res[int(v.PrivatePort)] = v
}

View File

@@ -1,34 +0,0 @@
package docker
import (
"encoding/json"
"github.com/yusing/go-proxy/internal/gperr"
)
type containerError struct {
errs *gperr.Builder
}
func (e *containerError) Add(err error) {
if e.errs == nil {
e.errs = gperr.NewBuilder()
}
e.errs.Add(err)
}
func (e *containerError) Error() string {
if e.errs == nil {
return "<niL>"
}
return e.errs.String()
}
func (e *containerError) Unwrap() error {
return e.errs.Error()
}
func (e *containerError) MarshalJSON() ([]byte, error) {
err := e.errs.Error().(interface{ Plain() []byte })
return json.Marshal(string(err.Plain()))
}

View File

@@ -1,5 +1,7 @@
package docker
import "github.com/yusing/go-proxy/internal/types"
var imageBlacklist = map[string]struct{}{
// pure databases without UI
"postgres": {},
@@ -45,7 +47,7 @@ var authorBlacklist = map[string]struct{}{
"docker": {},
}
func (image *ContainerImage) IsBlacklisted() bool {
func IsBlacklistedImage(image *types.ContainerImage) bool {
_, ok := imageBlacklist[image.Name]
if ok {
return true

View File

@@ -6,15 +6,14 @@ import (
"github.com/goccy/go-yaml"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type LabelMap = map[string]any
var ErrInvalidLabel = gperr.New("invalid label")
func ParseLabels(labels map[string]string, aliases ...string) (LabelMap, gperr.Error) {
nestedMap := make(LabelMap)
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, gperr.Error) {
nestedMap := make(types.LabelMap)
errs := gperr.NewBuilder("labels error")
ExpandWildcard(labels, aliases...)
@@ -38,15 +37,15 @@ func ParseLabels(labels map[string]string, aliases ...string) (LabelMap, gperr.E
} else {
// If the key doesn't exist, create a new map
if _, exists := currentMap[k]; !exists {
currentMap[k] = make(LabelMap)
currentMap[k] = make(types.LabelMap)
}
// Move deeper into the nested map
m, ok := currentMap[k].(LabelMap)
m, ok := currentMap[k].(types.LabelMap)
if !ok && currentMap[k] != "" {
errs.Add(gperr.Errorf("expect mapping, got %T", currentMap[k]).Subject(lbl))
continue
} else if !ok {
m = make(LabelMap)
m = make(types.LabelMap)
currentMap[k] = m
}
currentMap = m

View File

@@ -21,7 +21,7 @@ var listOptions = container.ListOptions{
All: true,
}
func ListContainers(clientHost string) ([]container.SummaryTrimmed, error) {
func ListContainers(clientHost string) ([]container.Summary, error) {
dockerClient, err := NewClient(clientHost)
if err != nil {
return nil, err