mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-25 10:18:59 +02:00
Merge branch 'main' into dev
This commit is contained in:
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
yusing@6uo.me.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
13
agent/go.mod
13
agent/go.mod
@@ -2,6 +2,11 @@ module github.com/yusing/godoxy/agent
|
|||||||
|
|
||||||
go 1.25.6
|
go 1.25.6
|
||||||
|
|
||||||
|
exclude (
|
||||||
|
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||||
|
github.com/moby/moby/client v0.2.2 // allow older daemon versions
|
||||||
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||||
github.com/yusing/godoxy => ../
|
github.com/yusing/godoxy => ../
|
||||||
@@ -22,7 +27,7 @@ require (
|
|||||||
github.com/pion/transport/v3 v3.1.1
|
github.com/pion/transport/v3 v3.1.1
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yusing/godoxy v0.25.0
|
github.com/yusing/godoxy v0.25.2
|
||||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
)
|
)
|
||||||
@@ -38,7 +43,7 @@ require (
|
|||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v29.1.5+incompatible // indirect
|
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
@@ -86,8 +91,8 @@ require (
|
|||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
github.com/yusing/ds v0.4.1 // indirect
|
github.com/yusing/ds v0.4.1 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.1.16 // indirect
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260125040745-bcc4b498f878 // indirect
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260125040745-bcc4b498f878 // indirect
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
|
|||||||
12
agent/go.sum
12
agent/go.sum
@@ -37,8 +37,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -82,8 +82,8 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
|
|||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -153,8 +153,8 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
|
|||||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY=
|
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||||
github.com/pires/go-proxyproto v0.9.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
@@ -150,7 +150,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
// test stream server connection
|
// test stream server connection
|
||||||
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
||||||
// test TCP stream support
|
// test TCP stream support
|
||||||
err := agentstream.TCPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
err := agentstream.TCPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -158,7 +158,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test UDP stream support
|
// test UDP stream support
|
||||||
err = agentstream.UDPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
err = agentstream.UDPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -313,8 +313,18 @@ func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeout := 5 * time.Second
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
remaining := time.Until(deadline)
|
||||||
|
if remaining > 0 {
|
||||||
|
timeout = remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Transport: cfg.Transport(),
|
Transport: cfg.Transport(),
|
||||||
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
return client.Do(req)
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
@@ -356,7 +366,7 @@ func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any)
|
|||||||
return resp.StatusCode, nil
|
return resp.StatusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(data, out)
|
err = sonic.Unmarshal(data, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stream
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
@@ -34,13 +35,13 @@ func NewTCPClient(serverAddr, targetAddress string, caCert *x509.Certificate, cl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
return newTCPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
func TCPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||||
header := NewStreamHealthCheckHeader()
|
header := NewStreamHealthCheckHeader()
|
||||||
|
|
||||||
conn, err := newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
conn, err := newTCPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
func newTCPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
// Setup TLS configuration
|
// Setup TLS configuration
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
caCertPool.AddCert(caCert)
|
caCertPool.AddCert(caCert)
|
||||||
@@ -62,17 +63,43 @@ func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCe
|
|||||||
ServerName: common.CertsDNSName,
|
ServerName: common.CertsDNSName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: dialTimeout,
|
||||||
|
}
|
||||||
|
tlsDialer := &tls.Dialer{
|
||||||
|
NetDialer: dialer,
|
||||||
|
Config: tlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
// Establish TLS connection
|
// Establish TLS connection
|
||||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: dialTimeout}, "tcp", serverAddr, tlsConfig)
|
conn, err := tlsDialer.DialContext(ctx, "tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deadline, hasDeadline := ctx.Deadline()
|
||||||
|
if hasDeadline {
|
||||||
|
err := conn.SetWriteDeadline(deadline)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
// Send the stream header once as a handshake.
|
// Send the stream header once as a handshake.
|
||||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasDeadline {
|
||||||
|
// reset write deadline
|
||||||
|
err = conn.SetWriteDeadline(time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &TCPClient{
|
return &TCPClient{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func TestTCPHealthCheck(t *testing.T) {
|
|||||||
|
|
||||||
srv := startTCPServer(t, certs)
|
srv := startTCPServer(t, certs)
|
||||||
|
|
||||||
err := stream.TCPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
err := stream.TCPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||||
require.NoError(t, err, "health check")
|
require.NoError(t, err, "health check")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +21,6 @@ func TestUDPHealthCheck(t *testing.T) {
|
|||||||
|
|
||||||
srv := startUDPServer(t, certs)
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
err := stream.UDPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
err := stream.UDPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||||
require.NoError(t, err, "health check")
|
require.NoError(t, err, "health check")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stream
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
@@ -35,10 +36,10 @@ func NewUDPClient(serverAddr, targetAddress string, caCert *x509.Certificate, cl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
return newUDPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
func newUDPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
// Setup DTLS configuration
|
// Setup DTLS configuration
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
caCertPool.AddCert(caCert)
|
caCertPool.AddCert(caCert)
|
||||||
@@ -62,21 +63,40 @@ func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deadline, hasDeadline := ctx.Deadline()
|
||||||
|
if hasDeadline {
|
||||||
|
err := conn.SetWriteDeadline(deadline)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send the stream header once as a handshake.
|
// Send the stream header once as a handshake.
|
||||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasDeadline {
|
||||||
|
// reset write deadline
|
||||||
|
err = conn.SetWriteDeadline(time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &UDPClient{
|
return &UDPClient{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UDPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
func UDPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||||
header := NewStreamHealthCheckHeader()
|
header := NewStreamHealthCheckHeader()
|
||||||
|
|
||||||
conn, err := newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
conn, err := newUDPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
29
go.mod
29
go.mod
@@ -2,6 +2,11 @@ module github.com/yusing/godoxy
|
|||||||
|
|
||||||
go 1.25.6
|
go 1.25.6
|
||||||
|
|
||||||
|
exclude (
|
||||||
|
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||||
|
github.com/moby/moby/client v0.2.2 // allow older daemon versions
|
||||||
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||||
github.com/luthermonson/go-proxmox => ./internal/go-proxmox
|
github.com/luthermonson/go-proxmox => ./internal/go-proxmox
|
||||||
@@ -25,7 +30,7 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||||
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||||
github.com/pires/go-proxyproto v0.9.1 // proxy protocol support
|
github.com/pires/go-proxyproto v0.9.2 // proxy protocol support
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
||||||
github.com/rs/zerolog v1.34.0 // logging
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||||
@@ -39,9 +44,9 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||||
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
||||||
github.com/docker/cli v29.1.5+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
github.com/docker/cli v29.2.0+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||||
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
|
||||||
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
||||||
github.com/moby/moby/api v1.52.0 // docker API
|
github.com/moby/moby/api v1.52.0 // docker API
|
||||||
github.com/moby/moby/client v0.2.1 // docker client
|
github.com/moby/moby/client v0.2.1 // docker client
|
||||||
@@ -52,13 +57,13 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1 // testing framework
|
github.com/stretchr/testify v1.11.1 // testing framework
|
||||||
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||||
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
||||||
github.com/yusing/godoxy/agent v0.0.0-20260125091326-9c2051840fd9
|
github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260124133347-9a96f3cc539e
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/gointernals v0.1.16
|
github.com/yusing/gointernals v0.1.16
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260125040745-bcc4b498f878
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260125040745-bcc4b498f878
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
|
||||||
github.com/yusing/goutils/server v0.0.0-20260125040745-bcc4b498f878
|
github.com/yusing/goutils/server v0.0.0-20260129081554-24e52ede7468
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -136,8 +141,8 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.41.0 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
google.golang.org/api v0.262.0 // indirect
|
google.golang.org/api v0.263.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||||
@@ -170,8 +175,8 @@ require (
|
|||||||
github.com/linode/linodego v1.64.0 // indirect
|
github.com/linode/linodego v1.64.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
|
|||||||
28
go.sum
28
go.sum
@@ -76,8 +76,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -137,8 +137,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
|||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -227,10 +227,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
|
|||||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 h1:+fx2mbWeR8XX/vidwpRMepJMtRIYQP44Iezm2oeObVM=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 h1:GDhBiaIAm/QXLzHJ0ASDdY/6R/9w60+gk8lY5rgfxEQ=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1/go.mod h1:EHScJdbM0gg5Is7e3C0ceRYAFMMsfP4Vf8sBRoxoTgk=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -251,8 +251,8 @@ github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
|||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY=
|
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||||
github.com/pires/go-proxyproto v0.9.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -447,14 +447,14 @@ golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
|
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
|
||||||
google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
|
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: 272bc53439...52ea531e95
@@ -27,6 +27,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
|
|||||||
AgentConfig: cfg,
|
AgentConfig: cfg,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
},
|
},
|
||||||
fasthttpHcClient: &fasthttp.Client{
|
fasthttpHcClient: &fasthttp.Client{
|
||||||
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
|
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
|||||||
route.GET("/providers", routeApi.Providers)
|
route.GET("/providers", routeApi.Providers)
|
||||||
route.GET("/by_provider", routeApi.ByProvider)
|
route.GET("/by_provider", routeApi.ByProvider)
|
||||||
route.POST("/playground", routeApi.Playground)
|
route.POST("/playground", routeApi.Playground)
|
||||||
|
route.GET("/validate", routeApi.Validate) // websocket
|
||||||
|
route.POST("/validate", routeApi.Validate)
|
||||||
}
|
}
|
||||||
|
|
||||||
file := v1.Group("/file")
|
file := v1.Group("/file")
|
||||||
|
|||||||
@@ -1087,7 +1087,7 @@
|
|||||||
"post": {
|
"post": {
|
||||||
"description": "Validate file",
|
"description": "Validate file",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"text/plain"
|
"application/yaml"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -3026,6 +3026,122 @@
|
|||||||
"operationId": "providers"
|
"operationId": "providers"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/route/validate": {
|
||||||
|
"get": {
|
||||||
|
"description": "Validate route,",
|
||||||
|
"consumes": [
|
||||||
|
"application/yaml"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"route",
|
||||||
|
"websocket"
|
||||||
|
],
|
||||||
|
"summary": "Validate route",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Route",
|
||||||
|
"name": "route",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Route"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Route validated",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/SuccessResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"417": {
|
||||||
|
"description": "Validation failed",
|
||||||
|
"schema": {}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-id": "validate",
|
||||||
|
"operationId": "validate"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Validate route,",
|
||||||
|
"consumes": [
|
||||||
|
"application/yaml"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"route",
|
||||||
|
"websocket"
|
||||||
|
],
|
||||||
|
"summary": "Validate route",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Route",
|
||||||
|
"name": "route",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Route"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Route validated",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/SuccessResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"417": {
|
||||||
|
"description": "Validation failed",
|
||||||
|
"schema": {}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-id": "validate",
|
||||||
|
"operationId": "validate"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/route/{which}": {
|
"/route/{which}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List route",
|
"description": "List route",
|
||||||
@@ -6745,229 +6861,6 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"route.Route": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"access_log": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/RequestLoggerConfig"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"agent": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"alias": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"bind": {
|
|
||||||
"description": "for TCP and UDP routes, bind address to listen on",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"container": {
|
|
||||||
"description": "Docker only",
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/Container"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"disable_compression": {
|
|
||||||
"type": "boolean",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"excluded": {
|
|
||||||
"type": "boolean",
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"excluded_reason": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"health": {
|
|
||||||
"description": "for swagger",
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/HealthJSON"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"healthcheck": {
|
|
||||||
"description": "null on load-balancer routes",
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/HealthCheckConfig"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"homepage": {
|
|
||||||
"$ref": "#/definitions/HomepageItemConfig",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"host": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"idlewatcher": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/IdlewatcherConfig"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"index": {
|
|
||||||
"description": "Index file to serve for single-page app mode",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"load_balance": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/LoadBalancerConfig"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"lurl": {
|
|
||||||
"description": "private fields",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"middlewares": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"$ref": "#/definitions/types.LabelMap"
|
|
||||||
},
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"no_tls_verify": {
|
|
||||||
"type": "boolean",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"path_patterns": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"port": {
|
|
||||||
"$ref": "#/definitions/Port",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"provider": {
|
|
||||||
"description": "for backward compatibility",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"proxmox": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/ProxmoxNodeConfig"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"purl": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"response_header_timeout": {
|
|
||||||
"type": "integer",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"rule_file": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/rules.Rule"
|
|
||||||
},
|
|
||||||
"x-nullable": true
|
|
||||||
},
|
|
||||||
"scheme": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"http",
|
|
||||||
"https",
|
|
||||||
"h2c",
|
|
||||||
"tcp",
|
|
||||||
"udp",
|
|
||||||
"fileserver"
|
|
||||||
],
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"spa": {
|
|
||||||
"description": "Single-page app mode: serves index for non-existent paths",
|
|
||||||
"type": "boolean",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"ssl_certificate": {
|
|
||||||
"description": "Path to client certificate",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"ssl_certificate_key": {
|
|
||||||
"description": "Path to client certificate key",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"ssl_protocols": {
|
|
||||||
"description": "Allowed TLS protocols",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"ssl_server_name": {
|
|
||||||
"description": "SSL/TLS proxy options (nginx-like)",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"ssl_trusted_certificate": {
|
|
||||||
"description": "Path to trusted CA certificates",
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"routeApi.RawRule": {
|
"routeApi.RawRule": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6995,7 +6888,7 @@
|
|||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/route.Route"
|
"$ref": "#/definitions/Route"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
|
|||||||
@@ -1807,12 +1807,12 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
kernel_version:
|
kernel_version:
|
||||||
type: string
|
type: string
|
||||||
|
load_avg_15m:
|
||||||
|
type: string
|
||||||
load_avg_1m:
|
load_avg_1m:
|
||||||
type: string
|
type: string
|
||||||
load_avg_5m:
|
load_avg_5m:
|
||||||
type: string
|
type: string
|
||||||
load_avg_15m:
|
|
||||||
type: string
|
|
||||||
mem_pct:
|
mem_pct:
|
||||||
type: string
|
type: string
|
||||||
mem_total:
|
mem_total:
|
||||||
@@ -1830,127 +1830,6 @@ definitions:
|
|||||||
uptime:
|
uptime:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
route.Route:
|
|
||||||
properties:
|
|
||||||
access_log:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/RequestLoggerConfig'
|
|
||||||
x-nullable: true
|
|
||||||
agent:
|
|
||||||
type: string
|
|
||||||
alias:
|
|
||||||
type: string
|
|
||||||
bind:
|
|
||||||
description: for TCP and UDP routes, bind address to listen on
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
|
||||||
container:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/Container'
|
|
||||||
description: Docker only
|
|
||||||
x-nullable: true
|
|
||||||
disable_compression:
|
|
||||||
type: boolean
|
|
||||||
excluded:
|
|
||||||
type: boolean
|
|
||||||
x-nullable: true
|
|
||||||
excluded_reason:
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
|
||||||
health:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/HealthJSON'
|
|
||||||
description: for swagger
|
|
||||||
healthcheck:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/HealthCheckConfig'
|
|
||||||
description: null on load-balancer routes
|
|
||||||
x-nullable: true
|
|
||||||
homepage:
|
|
||||||
$ref: '#/definitions/HomepageItemConfig'
|
|
||||||
host:
|
|
||||||
type: string
|
|
||||||
idlewatcher:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/IdlewatcherConfig'
|
|
||||||
x-nullable: true
|
|
||||||
index:
|
|
||||||
description: Index file to serve for single-page app mode
|
|
||||||
type: string
|
|
||||||
load_balance:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/LoadBalancerConfig'
|
|
||||||
x-nullable: true
|
|
||||||
lurl:
|
|
||||||
description: private fields
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
|
||||||
middlewares:
|
|
||||||
additionalProperties:
|
|
||||||
$ref: '#/definitions/types.LabelMap'
|
|
||||||
type: object
|
|
||||||
x-nullable: true
|
|
||||||
no_tls_verify:
|
|
||||||
type: boolean
|
|
||||||
path_patterns:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
x-nullable: true
|
|
||||||
port:
|
|
||||||
$ref: '#/definitions/Port'
|
|
||||||
provider:
|
|
||||||
description: for backward compatibility
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
|
||||||
proxmox:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/ProxmoxNodeConfig'
|
|
||||||
x-nullable: true
|
|
||||||
purl:
|
|
||||||
type: string
|
|
||||||
response_header_timeout:
|
|
||||||
type: integer
|
|
||||||
root:
|
|
||||||
type: string
|
|
||||||
rule_file:
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
|
||||||
rules:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/rules.Rule'
|
|
||||||
type: array
|
|
||||||
x-nullable: true
|
|
||||||
scheme:
|
|
||||||
enum:
|
|
||||||
- http
|
|
||||||
- https
|
|
||||||
- h2c
|
|
||||||
- tcp
|
|
||||||
- udp
|
|
||||||
- fileserver
|
|
||||||
type: string
|
|
||||||
spa:
|
|
||||||
description: 'Single-page app mode: serves index for non-existent paths'
|
|
||||||
type: boolean
|
|
||||||
ssl_certificate:
|
|
||||||
description: Path to client certificate
|
|
||||||
type: string
|
|
||||||
ssl_certificate_key:
|
|
||||||
description: Path to client certificate key
|
|
||||||
type: string
|
|
||||||
ssl_protocols:
|
|
||||||
description: Allowed TLS protocols
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
ssl_server_name:
|
|
||||||
description: SSL/TLS proxy options (nginx-like)
|
|
||||||
type: string
|
|
||||||
ssl_trusted_certificate:
|
|
||||||
description: Path to trusted CA certificates
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
routeApi.RawRule:
|
routeApi.RawRule:
|
||||||
properties:
|
properties:
|
||||||
do:
|
do:
|
||||||
@@ -1963,7 +1842,7 @@ definitions:
|
|||||||
routeApi.RoutesByProvider:
|
routeApi.RoutesByProvider:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/route.Route'
|
$ref: '#/definitions/Route'
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
rules.Rule:
|
rules.Rule:
|
||||||
@@ -2741,7 +2620,7 @@ paths:
|
|||||||
/file/validate:
|
/file/validate:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- text/plain
|
- application/yaml
|
||||||
description: Validate file
|
description: Validate file
|
||||||
parameters:
|
parameters:
|
||||||
- description: Type
|
- description: Type
|
||||||
@@ -4079,6 +3958,83 @@ paths:
|
|||||||
- route
|
- route
|
||||||
- websocket
|
- websocket
|
||||||
x-id: providers
|
x-id: providers
|
||||||
|
/route/validate:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/yaml
|
||||||
|
description: Validate route,
|
||||||
|
parameters:
|
||||||
|
- description: Route
|
||||||
|
in: body
|
||||||
|
name: route
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Route'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Route validated
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/SuccessResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"417":
|
||||||
|
description: Validation failed
|
||||||
|
schema: {}
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
summary: Validate route
|
||||||
|
tags:
|
||||||
|
- route
|
||||||
|
- websocket
|
||||||
|
x-id: validate
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/yaml
|
||||||
|
description: Validate route,
|
||||||
|
parameters:
|
||||||
|
- description: Route
|
||||||
|
in: body
|
||||||
|
name: route
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Route'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Route validated
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/SuccessResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"417":
|
||||||
|
description: Validation failed
|
||||||
|
schema: {}
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
summary: Validate route
|
||||||
|
tags:
|
||||||
|
- route
|
||||||
|
- websocket
|
||||||
|
x-id: validate
|
||||||
/stats:
|
/stats:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type ValidateFileRequest struct {
|
|||||||
// @Summary Validate file
|
// @Summary Validate file
|
||||||
// @Description Validate file
|
// @Description Validate file
|
||||||
// @Tags file
|
// @Tags file
|
||||||
// @Accept text/plain
|
// @Accept application/yaml
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param type query FileType true "Type"
|
// @Param type query FileType true "Type"
|
||||||
// @Param file body string true "File content"
|
// @Param file body string true "File content"
|
||||||
@@ -29,7 +29,7 @@ type ValidateFileRequest struct {
|
|||||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||||
// @Failure 417 {object} any "Validation failed"
|
// @Failure 417 {object} any "Validation failed"
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
// @Router /file/validate [post]
|
// @Router /file/validate [post]
|
||||||
func Validate(c *gin.Context) {
|
func Validate(c *gin.Context) {
|
||||||
var request ValidateFileRequest
|
var request ValidateFileRequest
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
|
|||||||
69
internal/api/v1/route/validate.go
Normal file
69
internal/api/v1/route/validate.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package routeApi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/yusing/godoxy/internal/route"
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type _ = route.Route
|
||||||
|
|
||||||
|
// @x-id "validate"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Validate route
|
||||||
|
// @Description Validate route,
|
||||||
|
// @Tags route,websocket
|
||||||
|
// @Accept application/yaml
|
||||||
|
// @Produce json
|
||||||
|
// @Param route body route.Route true "Route"
|
||||||
|
// @Success 200 {object} apitypes.SuccessResponse "Route validated"
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Bad request"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||||
|
// @Failure 417 {object} any "Validation failed"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
|
// @Router /route/validate [get]
|
||||||
|
// @Router /route/validate [post]
|
||||||
|
func Validate(c *gin.Context) {
|
||||||
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
|
ValidateWS(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request route.Route
|
||||||
|
if err := c.ShouldBindWith(&request, serialization.GinYAMLBinding{}); err != nil {
|
||||||
|
c.JSON(http.StatusExpectationFailed, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, apitypes.Success("route validated"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateWS(c *gin.Context) {
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
const writeTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-manager.Done():
|
||||||
|
return
|
||||||
|
case msg := <-manager.ReadCh():
|
||||||
|
var request route.Route
|
||||||
|
if err := serialization.UnmarshalValidate(msg, &request, yaml.Unmarshal); err != nil {
|
||||||
|
manager.WriteJSON(gin.H{"error": err}, writeTimeout)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
manager.WriteJSON(gin.H{"message": "route validated"}, writeTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||||
@@ -25,9 +26,9 @@ func TestEABConfigRequired(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
yaml := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
yamlCfg := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||||
cfg := autocert.Config{}
|
cfg := autocert.Config{}
|
||||||
err := serialization.UnmarshalValidateYAML(yaml, &cfg)
|
err := serialization.UnmarshalValidate(yamlCfg, &cfg, yaml.Unmarshal)
|
||||||
if (err != nil) != test.wantErr {
|
if (err != nil) != test.wantErr {
|
||||||
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
@@ -41,7 +42,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
|
|||||||
cfg.HTTPClient = acmeServer.httpClient()
|
cfg.HTTPClient = acmeServer.httpClient()
|
||||||
|
|
||||||
/* unmarshal yaml config with multiple certs */
|
/* unmarshal yaml config with multiple certs */
|
||||||
err := error(serialization.UnmarshalValidateYAML(yamlConfig, &cfg))
|
err := error(serialization.UnmarshalValidate(yamlConfig, &cfg, yaml.Unmarshal))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
||||||
require.Len(t, cfg.Extra, 2)
|
require.Len(t, cfg.Extra, 2)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package autocert_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||||
@@ -42,7 +43,7 @@ extra:
|
|||||||
`
|
`
|
||||||
|
|
||||||
var cfg autocert.Config
|
var cfg autocert.Config
|
||||||
err := error(serialization.UnmarshalValidateYAML([]byte(cfgYAML), &cfg))
|
err := error(serialization.UnmarshalValidate([]byte(cfgYAML), &cfg, yaml.Unmarshal))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test: extra[0] inherits all fields from main except CertPath and KeyPath.
|
// Test: extra[0] inherits all fields from main except CertPath and KeyPath.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (state *state) InitFromFile(filename string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) Init(data []byte) error {
|
func (state *state) Init(data []byte) error {
|
||||||
err := serialization.UnmarshalValidateYAML(data, &state.Config)
|
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
@@ -43,7 +44,7 @@ type (
|
|||||||
|
|
||||||
func Validate(data []byte) gperr.Error {
|
func Validate(data []byte) gperr.Error {
|
||||||
var model Config
|
var model Config
|
||||||
return serialization.UnmarshalValidateYAML(data, &model)
|
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ replace github.com/yusing/godoxy => ../..
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-acme/lego/v4 v4.31.0
|
github.com/go-acme/lego/v4 v4.31.0
|
||||||
github.com/yusing/godoxy v0.25.0
|
github.com/yusing/godoxy v0.25.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -44,7 +44,7 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gofrs/flock v0.13.0 // indirect
|
github.com/gofrs/flock v0.13.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
@@ -65,8 +65,8 @@ require (
|
|||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
|
||||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
@@ -98,8 +98,8 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.41.0 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
google.golang.org/api v0.262.0 // indirect
|
google.golang.org/api v0.263.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
|||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -150,10 +150,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
|
|||||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 h1:+fx2mbWeR8XX/vidwpRMepJMtRIYQP44Iezm2oeObVM=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 h1:GDhBiaIAm/QXLzHJ0ASDdY/6R/9w60+gk8lY5rgfxEQ=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1/go.mod h1:EHScJdbM0gg5Is7e3C0ceRYAFMMsfP4Vf8sBRoxoTgk=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||||
@@ -249,14 +249,14 @@ golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
|||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
|
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
|
||||||
google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
|
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -76,8 +76,11 @@ func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Du
|
|||||||
|
|
||||||
setCommonHeaders(req.Header.Set)
|
setCommonHeaders(req.Header.Set)
|
||||||
|
|
||||||
|
client := *h2cClient
|
||||||
|
client.Timeout = timeout
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resp, err := h2cClient.Do(req)
|
resp, err := client.Do(req)
|
||||||
lat := time.Since(start)
|
lat := time.Since(start)
|
||||||
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
|
|||||||
@@ -55,20 +55,20 @@ func init() {
|
|||||||
|
|
||||||
func InitCache() {
|
func InitCache() {
|
||||||
m := make(IconMap)
|
m := make(IconMap)
|
||||||
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
|
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, sonic.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// backward compatible
|
// backward compatible
|
||||||
oldFormat := struct {
|
oldFormat := struct {
|
||||||
Icons IconMap
|
Icons IconMap
|
||||||
LastUpdate time.Time
|
LastUpdate time.Time
|
||||||
}{}
|
}{}
|
||||||
err = serialization.LoadJSONIfExist(common.IconListCachePath, &oldFormat)
|
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, sonic.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to load icons")
|
log.Error().Err(err).Msg("failed to load icons")
|
||||||
} else {
|
} else {
|
||||||
m = oldFormat.Icons
|
m = oldFormat.Icons
|
||||||
// store it to disk immediately
|
// store it to disk immediately
|
||||||
_ = serialization.SaveJSON(common.IconListCachePath, &m, 0o644)
|
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, sonic.Marshal)
|
||||||
}
|
}
|
||||||
} else if len(m) > 0 {
|
} else if len(m) > 0 {
|
||||||
log.Info().
|
log.Info().
|
||||||
@@ -84,7 +84,7 @@ func InitCache() {
|
|||||||
|
|
||||||
task.OnProgramExit("save_icons_cache", func() {
|
task.OnProgramExit("save_icons_cache", func() {
|
||||||
icons := iconsCache.Load()
|
icons := iconsCache.Load()
|
||||||
_ = serialization.SaveJSON(common.IconListCachePath, &icons, 0o644)
|
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, sonic.Marshal)
|
||||||
})
|
})
|
||||||
|
|
||||||
go backgroundUpdateIcons()
|
go backgroundUpdateIcons()
|
||||||
@@ -105,7 +105,7 @@ func backgroundUpdateIcons() {
|
|||||||
// swap old cache with new cache
|
// swap old cache with new cache
|
||||||
iconsCache.Store(newCache)
|
iconsCache.Store(newCache)
|
||||||
// save it to disk
|
// save it to disk
|
||||||
err := serialization.SaveJSON(common.IconListCachePath, &newCache, 0o644)
|
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, sonic.Marshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("failed to save icons")
|
log.Warn().Err(err).Msg("failed to save icons")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ func loadNS[T store](ns namespace) T {
|
|||||||
func save() error {
|
func save() error {
|
||||||
errs := gperr.NewBuilder("failed to save data stores")
|
errs := gperr.NewBuilder("failed to save data stores")
|
||||||
for ns, store := range stores {
|
for ns, store := range stores {
|
||||||
if err := serialization.SaveJSON(filepath.Join(storesPath, string(ns)+".json"), &store, 0o644); err != nil {
|
path := filepath.Join(storesPath, string(ns)+".json")
|
||||||
|
if err := serialization.SaveFile(path, &store, 0o644, sonic.Marshal); err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type Client struct {
|
|||||||
*proxmox.Client
|
*proxmox.Client
|
||||||
*proxmox.Cluster
|
*proxmox.Cluster
|
||||||
Version *proxmox.Version
|
Version *proxmox.Version
|
||||||
|
BaseURL *url.URL
|
||||||
// id -> resource; id: lxc/<vmid> or qemu/<vmid>
|
// id -> resource; id: lxc/<vmid> or qemu/<vmid>
|
||||||
resources map[string]*VMResource
|
resources map[string]*VMResource
|
||||||
resourcesMu sync.RWMutex
|
resourcesMu sync.RWMutex
|
||||||
@@ -79,6 +80,9 @@ type VMResource struct {
|
|||||||
*proxmox.ClusterResource
|
*proxmox.ClusterResource
|
||||||
IPs []net.IP
|
IPs []net.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Proxmox client.
|
||||||
|
func NewClient(baseUrl string, opts ...proxmox.Option) *Client
|
||||||
```
|
```
|
||||||
|
|
||||||
### Node
|
### Node
|
||||||
@@ -97,10 +101,11 @@ var Nodes = pool.New[*Node]("proxmox_nodes")
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
type NodeConfig struct {
|
type NodeConfig struct {
|
||||||
Node string `json:"node" validate:"required"`
|
Node string `json:"node" validate:"required"`
|
||||||
VMID int `json:"vmid" validate:"required"`
|
VMID *int `json:"vmid"` // nil: auto discover; 0: node-level route; >0: lxc/qemu resource route
|
||||||
VMName string `json:"vmname,omitempty"`
|
VMName string `json:"vmname,omitempty"`
|
||||||
Service string `json:"service,omitempty"`
|
Services []string `json:"services,omitempty" aliases:"service"`
|
||||||
|
Files []string `json:"files,omitempty" aliases:"file"`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -119,6 +124,9 @@ func (c *Config) Client() *Client
|
|||||||
### Client Operations
|
### Client Operations
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
// NewClient creates a new Proxmox client.
|
||||||
|
func NewClient(baseUrl string, opts ...proxmox.Option) *Client
|
||||||
|
|
||||||
// UpdateClusterInfo fetches cluster info and discovers nodes.
|
// UpdateClusterInfo fetches cluster info and discovers nodes.
|
||||||
func (c *Client) UpdateClusterInfo(ctx context.Context) error
|
func (c *Client) UpdateClusterInfo(ctx context.Context) error
|
||||||
|
|
||||||
@@ -136,6 +144,15 @@ func (c *Client) ReverseLookupNode(hostname string, ip net.IP, alias string) str
|
|||||||
|
|
||||||
// NumNodes returns the number of nodes in the cluster.
|
// NumNodes returns the number of nodes in the cluster.
|
||||||
func (c *Client) NumNodes() int
|
func (c *Client) NumNodes() int
|
||||||
|
|
||||||
|
// Key returns the cluster ID.
|
||||||
|
func (c *Client) Key() string
|
||||||
|
|
||||||
|
// Name returns the cluster name.
|
||||||
|
func (c *Client) Name() string
|
||||||
|
|
||||||
|
// MarshalJSON returns the cluster info as JSON.
|
||||||
|
func (c *Client) MarshalJSON() ([]byte, error)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Node Operations
|
### Node Operations
|
||||||
@@ -144,17 +161,29 @@ func (c *Client) NumNodes() int
|
|||||||
// AvailableNodeNames returns all available node names as a comma-separated string.
|
// AvailableNodeNames returns all available node names as a comma-separated string.
|
||||||
func AvailableNodeNames() string
|
func AvailableNodeNames() string
|
||||||
|
|
||||||
|
// NewNode creates a new node.
|
||||||
|
func NewNode(client *Client, name, id string) *Node
|
||||||
|
|
||||||
// Node.Client returns the Proxmox client.
|
// Node.Client returns the Proxmox client.
|
||||||
func (n *Node) Client() *Client
|
func (n *Node) Client() *Client
|
||||||
|
|
||||||
// Node.Get performs a GET request on the node.
|
// Node.Get performs a GET request on the node.
|
||||||
func (n *Node) Get(ctx context.Context, path string, v any) error
|
func (n *Node) Get(ctx context.Context, path string, v any) error
|
||||||
|
|
||||||
|
// Node.Key returns the node name.
|
||||||
|
func (n *Node) Key() string
|
||||||
|
|
||||||
|
// Node.Name returns the node name.
|
||||||
|
func (n *Node) Name() string
|
||||||
|
|
||||||
// NodeCommand executes a command on the node and streams output.
|
// NodeCommand executes a command on the node and streams output.
|
||||||
func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser, error)
|
func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser, error)
|
||||||
|
|
||||||
// NodeJournalctl streams journalctl output from the node.
|
// NodeJournalctl streams journalctl output from the node.
|
||||||
func (n *Node) NodeJournalctl(ctx context.Context, service string, limit int) (io.ReadCloser, error)
|
func (n *Node) NodeJournalctl(ctx context.Context, services []string, limit int) (io.ReadCloser, error)
|
||||||
|
|
||||||
|
// NodeTail streams tail output for the given file.
|
||||||
|
func (n *Node) NodeTail(ctx context.Context, files []string, limit int) (io.ReadCloser, error)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -275,7 +304,35 @@ func (node *Node) LXCStats(ctx context.Context, vmid int, stream bool) (io.ReadC
|
|||||||
func (node *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.ReadCloser, error)
|
func (node *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.ReadCloser, error)
|
||||||
|
|
||||||
// LXCJournalctl streams journalctl output for a container service.
|
// LXCJournalctl streams journalctl output for a container service.
|
||||||
func (node *Node) LXCJournalctl(ctx context.Context, vmid int, service string, limit int) (io.ReadCloser, error)
|
// On non-systemd systems, it falls back to tailing /var/log/messages.
|
||||||
|
func (node *Node) LXCJournalctl(ctx context.Context, vmid int, services []string, limit int) (io.ReadCloser, error)
|
||||||
|
|
||||||
|
// LXCTail streams tail output for the given file.
|
||||||
|
func (node *Node) LXCTail(ctx context.Context, vmid int, files []string, limit int) (io.ReadCloser, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Stats
|
||||||
|
|
||||||
|
```go
|
||||||
|
type NodeStats struct {
|
||||||
|
KernelVersion string `json:"kernel_version"`
|
||||||
|
PVEVersion string `json:"pve_version"`
|
||||||
|
CPUUsage string `json:"cpu_usage"`
|
||||||
|
CPUModel string `json:"cpu_model"`
|
||||||
|
MemUsage string `json:"mem_usage"`
|
||||||
|
MemTotal string `json:"mem_total"`
|
||||||
|
MemPct string `json:"mem_pct"`
|
||||||
|
RootFSUsage string `json:"rootfs_usage"`
|
||||||
|
RootFSTotal string `json:"rootfs_total"`
|
||||||
|
RootFSPct string `json:"rootfs_pct"`
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
LoadAvg1m string `json:"load_avg_1m"`
|
||||||
|
LoadAvg5m string `json:"load_avg_5m"`
|
||||||
|
LoadAvg15m string `json:"load_avg_15m"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeStats streams node statistics like docker stats.
|
||||||
|
func (n *Node) NodeStats(ctx context.Context, stream bool) (io.ReadCloser, error)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Flow
|
## Data Flow
|
||||||
@@ -453,6 +510,12 @@ var (
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| Error | Description |
|
||||||
|
| --------------------- | --------------------------------------------------------------------- |
|
||||||
|
| `ErrResourceNotFound` | Resource not found in cluster |
|
||||||
|
| `ErrNoResources` | No resources available |
|
||||||
|
| `ErrNoSession` | No session for WebSocket operations (requires username/password auth) |
|
||||||
|
|
||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
- Cluster info fetched once on init
|
- Cluster info fetched once on init
|
||||||
@@ -463,10 +526,26 @@ var (
|
|||||||
- Per-operation API calls with 3-second timeout
|
- Per-operation API calls with 3-second timeout
|
||||||
- WebSocket connections properly closed to prevent goroutine leaks
|
- WebSocket connections properly closed to prevent goroutine leaks
|
||||||
|
|
||||||
|
## Command Validation
|
||||||
|
|
||||||
|
Commands executed via WebSocket are validated to prevent command injection. Invalid characters include:
|
||||||
|
|
||||||
|
```
|
||||||
|
& | $ ; ' " ` $( ${ < >
|
||||||
|
```
|
||||||
|
|
||||||
|
Services and files passed to `journalctl` and `tail` commands are automatically validated.
|
||||||
|
|
||||||
## Constants
|
## Constants
|
||||||
|
|
||||||
```go
|
```go
|
||||||
const ResourcePollInterval = 3 * time.Second
|
const ResourcePollInterval = 3 * time.Second
|
||||||
|
const SessionRefreshInterval = 1 * time.Minute
|
||||||
|
const NodeStatsPollInterval = time.Second
|
||||||
```
|
```
|
||||||
|
|
||||||
The `ResourcePollInterval` constant controls how often resources are updated in the background loop.
|
| Constant | Default | Description |
|
||||||
|
| ------------------------ | ------- | ---------------------------------- |
|
||||||
|
| `ResourcePollInterval` | 3s | How often VM resources are updated |
|
||||||
|
| `SessionRefreshInterval` | 1m | How often sessions are refreshed |
|
||||||
|
| `NodeStatsPollInterval` | 1s | How often node stats are streamed |
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/luthermonson/go-proxmox"
|
"github.com/luthermonson/go-proxmox"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,7 +119,6 @@ func (c *Client) UpdateResources(ctx context.Context) error {
|
|||||||
c.resources[resource.ID] = vmResources[i]
|
c.resources[resource.ID] = vmResources[i]
|
||||||
}
|
}
|
||||||
c.resourcesMu.Unlock()
|
c.resourcesMu.Unlock()
|
||||||
log.Debug().Str("cluster", c.Cluster.Name).Msgf("[proxmox] updated %d resources", len(c.resources))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,15 @@ func formatTail(files []string, limit int) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var command strings.Builder
|
var command strings.Builder
|
||||||
command.WriteString("tail -f -q --retry ")
|
command.WriteString("tail -f -q ")
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
fmt.Fprintf(&command, " %q ", file)
|
fmt.Fprintf(&command, " %q ", file)
|
||||||
}
|
}
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
fmt.Fprintf(&command, " -n %d", limit)
|
fmt.Fprintf(&command, " -n %d", limit)
|
||||||
}
|
}
|
||||||
return command.String(), nil
|
// try --retry first, if it fails, try the command again
|
||||||
|
return fmt.Sprintf("sh -c '%s --retry 2>/dev/null || %s'", command.String(), command.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatJournalctl(services []string, limit int) (string, error) {
|
func formatJournalctl(services []string, limit int) (string, error) {
|
||||||
|
|||||||
@@ -162,4 +162,4 @@ func (c *Config) refreshSessionLoop(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.Rea
|
|||||||
|
|
||||||
// LXCJournalctl streams journalctl output for the given service.
|
// LXCJournalctl streams journalctl output for the given service.
|
||||||
//
|
//
|
||||||
|
// On non systemd systems, it will tail /var/log/messages as fallback.
|
||||||
|
//
|
||||||
// If services are not empty, it will be used to filter the output by service.
|
// If services are not empty, it will be used to filter the output by service.
|
||||||
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||||
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, services []string, limit int) (io.ReadCloser, error) {
|
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, services []string, limit int) (io.ReadCloser, error) {
|
||||||
@@ -46,6 +48,11 @@ func (n *Node) LXCJournalctl(ctx context.Context, vmid int, services []string, l
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(services) == 0 {
|
||||||
|
// add /var/log/messages fallback for non systemd systems
|
||||||
|
// in tail command, try --retry first, if it fails, try the command again
|
||||||
|
command = fmt.Sprintf("sh -c '%s 2>/dev/null || tail -f -q --retry /var/log/messages 2>/dev/null || tail -f -q /var/log/messages'", command)
|
||||||
|
}
|
||||||
return n.LXCCommand(ctx, vmid, command)
|
return n.LXCCommand(ctx, vmid, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/pool"
|
"github.com/yusing/goutils/pool"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +26,22 @@ type Node struct {
|
|||||||
// statsScriptInitErrs *xsync.Map[int, error]
|
// statsScriptInitErrs *xsync.Map[int, error]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate implements the serialization.CustomValidator interface.
|
||||||
|
func (n *NodeConfig) Validate() gperr.Error {
|
||||||
|
var errs gperr.Builder
|
||||||
|
for i, service := range n.Services {
|
||||||
|
if err := checkValidInput(service); err != nil {
|
||||||
|
errs.AddSubjectf(err, "services[%d]", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, file := range n.Files {
|
||||||
|
if err := checkValidInput(file); err != nil {
|
||||||
|
errs.AddSubjectf(err, "files[%d]", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
|
||||||
var Nodes = pool.New[*Node]("proxmox_nodes")
|
var Nodes = pool.New[*Node]("proxmox_nodes")
|
||||||
|
|
||||||
func NewNode(client *Client, name, id string) *Node {
|
func NewNode(client *Client, name, id string) *Node {
|
||||||
|
|||||||
56
internal/proxmox/validation_test.go
Normal file
56
internal/proxmox/validation_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package proxmox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateCommandArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
yamlCfg string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid_services",
|
||||||
|
yamlCfg: `services: ["foo", "bar"]`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_services",
|
||||||
|
yamlCfg: `services: ["foo", "bar & baz"]`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_services_with_$(",
|
||||||
|
yamlCfg: `services: ["foo", "bar & $(echo 'hello')"]`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid_files",
|
||||||
|
yamlCfg: `files: ["foo", "bar"]`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_files",
|
||||||
|
yamlCfg: `files: ["foo", "bar & baz"]`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var cfg NodeConfig
|
||||||
|
err := serialization.UnmarshalValidate([]byte(tt.yamlCfg), &cfg, yaml.Unmarshal)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorContains(t, err, "input contains invalid characters")
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
@@ -43,7 +44,7 @@ func removeXPrefix(m map[string]any) gperr.Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validate(data []byte) (routes route.Routes, err gperr.Error) {
|
func validate(data []byte) (routes route.Routes, err gperr.Error) {
|
||||||
err = serialization.UnmarshalValidateYAMLIntercept(data, &routes, removeXPrefix)
|
err = serialization.UnmarshalValidate(data, &routes, yaml.Unmarshal, removeXPrefix)
|
||||||
return routes, err
|
return routes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ type (
|
|||||||
Idlewatcher *types.IdlewatcherConfig `json:"idlewatcher,omitempty" extensions:"x-nullable"`
|
Idlewatcher *types.IdlewatcherConfig `json:"idlewatcher,omitempty" extensions:"x-nullable"`
|
||||||
|
|
||||||
Metadata `deserialize:"-"`
|
Metadata `deserialize:"-"`
|
||||||
}
|
} // @name Route
|
||||||
|
|
||||||
Metadata struct {
|
Metadata struct {
|
||||||
/* Docker only */
|
/* Docker only */
|
||||||
@@ -199,6 +199,7 @@ func (r *Route) validate() gperr.Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
|
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
|
||||||
|
wasNotNil := r.Proxmox != nil
|
||||||
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
|
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
|
||||||
if len(proxmoxProviders) > 0 {
|
if len(proxmoxProviders) > 0 {
|
||||||
// it's fine if ip is nil
|
// it's fine if ip is nil
|
||||||
@@ -239,88 +240,13 @@ func (r *Route) validate() gperr.Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if wasNotNil && (r.Proxmox.Node == "" || r.Proxmox.VMID == nil) {
|
||||||
|
log.Warn().Msgf("no proxmox node / resource found for route %q", r.Alias)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Proxmox != nil {
|
if r.Proxmox != nil {
|
||||||
nodeName := r.Proxmox.Node
|
r.validateProxmox()
|
||||||
vmid := r.Proxmox.VMID
|
|
||||||
if nodeName == "" || vmid == nil {
|
|
||||||
return gperr.Errorf("node (proxmox node name) is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
node, ok := proxmox.Nodes.Get(nodeName)
|
|
||||||
if !ok {
|
|
||||||
return gperr.Errorf("proxmox node %s not found in pool", nodeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node-level route (VMID = 0)
|
|
||||||
if *vmid == 0 {
|
|
||||||
r.Scheme = route.SchemeHTTPS
|
|
||||||
if r.Host == DefaultHost {
|
|
||||||
r.Host = node.Client().BaseURL.Hostname()
|
|
||||||
}
|
|
||||||
port, _ := strconv.Atoi(node.Client().BaseURL.Port())
|
|
||||||
if port == 0 {
|
|
||||||
port = 8006
|
|
||||||
}
|
|
||||||
r.Port.Proxy = port
|
|
||||||
} else {
|
|
||||||
res, err := node.Client().GetResource("lxc", *vmid)
|
|
||||||
if err != nil {
|
|
||||||
return gperr.Wrap(err) // ErrResourceNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Proxmox.VMName = res.Name
|
|
||||||
|
|
||||||
if r.Host == DefaultHost {
|
|
||||||
containerName := res.Name
|
|
||||||
// get ip addresses of the vmid
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
ips := res.IPs
|
|
||||||
if len(ips) == 0 {
|
|
||||||
return gperr.Multiline().
|
|
||||||
Addf("no ip addresses found for %s", containerName).
|
|
||||||
Adds("make sure you have set static ip address for container instead of dhcp").
|
|
||||||
Subject(containerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
l := log.With().Str("container", containerName).Logger()
|
|
||||||
|
|
||||||
l.Info().Msg("checking if container is running")
|
|
||||||
running, err := node.LXCIsRunning(ctx, *vmid)
|
|
||||||
if err != nil {
|
|
||||||
return gperr.New("failed to check container state").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !running {
|
|
||||||
l.Info().Msg("starting container")
|
|
||||||
if err := node.LXCAction(ctx, *vmid, proxmox.LXCStart); err != nil {
|
|
||||||
return gperr.New("failed to start container").With(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Info().Msgf("finding reachable ip addresses")
|
|
||||||
errs := gperr.NewBuilder("failed to find reachable ip addresses")
|
|
||||||
for _, ip := range ips {
|
|
||||||
if err := netutils.PingTCP(ctx, ip, r.Port.Proxy); err != nil {
|
|
||||||
errs.Add(gperr.Unwrap(err).Subjectf("%s:%d", ip, r.Port.Proxy))
|
|
||||||
} else {
|
|
||||||
r.Host = ip.String()
|
|
||||||
l.Info().Msgf("using ip %s", r.Host)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if r.Host == DefaultHost {
|
|
||||||
return gperr.Multiline().
|
|
||||||
Addf("no reachable ip addresses found, tried %d IPs", len(ips)).
|
|
||||||
With(errs.Error()).
|
|
||||||
Subject(containerName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Container != nil && r.Container.IdlewatcherConfig != nil {
|
if r.Container != nil && r.Container.IdlewatcherConfig != nil {
|
||||||
@@ -470,6 +396,90 @@ func (r *Route) validateRules() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Route) validateProxmox() {
|
||||||
|
l := log.With().Str("route", r.Alias).Logger()
|
||||||
|
|
||||||
|
nodeName := r.Proxmox.Node
|
||||||
|
vmid := r.Proxmox.VMID
|
||||||
|
if nodeName == "" || vmid == nil {
|
||||||
|
l.Error().Msg("node (proxmox node name) is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(nodeName)
|
||||||
|
if !ok {
|
||||||
|
l.Error().Msgf("proxmox node %s not found in pool", nodeName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node-level route (VMID = 0)
|
||||||
|
if *vmid == 0 {
|
||||||
|
r.Scheme = route.SchemeHTTPS
|
||||||
|
if r.Host == DefaultHost {
|
||||||
|
r.Host = node.Client().BaseURL.Hostname()
|
||||||
|
}
|
||||||
|
port, _ := strconv.Atoi(node.Client().BaseURL.Port())
|
||||||
|
if port == 0 {
|
||||||
|
port = 8006
|
||||||
|
}
|
||||||
|
r.Port.Proxy = port
|
||||||
|
} else {
|
||||||
|
res, err := node.Client().GetResource("lxc", *vmid)
|
||||||
|
if err != nil { // ErrResourceNotFound
|
||||||
|
l.Err(err).Msgf("failed to get resource %d", *vmid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Proxmox.VMName = res.Name
|
||||||
|
|
||||||
|
if r.Host == DefaultHost {
|
||||||
|
containerName := res.Name
|
||||||
|
// get ip addresses of the vmid
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ips := res.IPs
|
||||||
|
if len(ips) == 0 {
|
||||||
|
l.Warn().Msgf("no ip addresses found for %s, make sure you have set static ip address for container instead of dhcp", containerName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.With().Str("container", containerName).Logger()
|
||||||
|
|
||||||
|
l.Info().Msgf("checking if container is running")
|
||||||
|
running, err := node.LXCIsRunning(ctx, *vmid)
|
||||||
|
if err != nil {
|
||||||
|
l.Err(err).Msgf("failed to check container state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !running {
|
||||||
|
l.Info().Msgf("starting container")
|
||||||
|
if err := node.LXCAction(ctx, *vmid, proxmox.LXCStart); err != nil {
|
||||||
|
l.Err(err).Msgf("failed to start container")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info().Msgf("finding reachable ip addresses")
|
||||||
|
errs := gperr.NewBuilder("failed to find reachable ip addresses")
|
||||||
|
for _, ip := range ips {
|
||||||
|
if err := netutils.PingTCP(ctx, ip, r.Port.Proxy); err != nil {
|
||||||
|
errs.Add(gperr.Unwrap(err).Subjectf("%s:%d", ip, r.Port.Proxy))
|
||||||
|
} else {
|
||||||
|
r.Host = ip.String()
|
||||||
|
l.Info().Msgf("using ip %s", r.Host)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.Host == DefaultHost {
|
||||||
|
l.Warn().Err(errs.Error()).Msgf("no reachable ip addresses found, tried %d IPs", len(ips))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Route) Impl() types.Route {
|
func (r *Route) Impl() types.Route {
|
||||||
return r.impl
|
return r.impl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func appendRuleError(rm *httputils.ResponseModifier, rule *Rule, err error) {
|
func appendRuleError(rm *httputils.ResponseModifier, rule *Rule, err error) {
|
||||||
rm.AppendError("rule: %s, error: %w", rule.Name, err)
|
// rm.AppendError("rule: %s, error: %w", rule.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTerminatingHandler(handler CommandHandler) bool {
|
func isTerminatingHandler(handler CommandHandler) bool {
|
||||||
|
|||||||
@@ -115,24 +115,6 @@ func validateURL(args []string) (any, gperr.Error) {
|
|||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAbsoluteURL returns types.URL with the URL validated.
|
|
||||||
func validateAbsoluteURL(args []string) (any, gperr.Error) {
|
|
||||||
if len(args) != 1 {
|
|
||||||
return nil, ErrExpectOneArg
|
|
||||||
}
|
|
||||||
u, err := nettypes.ParseURL(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInvalidArguments.With(err)
|
|
||||||
}
|
|
||||||
if u.Scheme == "" {
|
|
||||||
u.Scheme = "http"
|
|
||||||
}
|
|
||||||
if u.Host == "" {
|
|
||||||
return nil, ErrInvalidArguments.Withf("missing host")
|
|
||||||
}
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateCIDR returns types.CIDR with the CIDR validated.
|
// validateCIDR returns types.CIDR with the CIDR validated.
|
||||||
func validateCIDR(args []string) (any, gperr.Error) {
|
func validateCIDR(args []string) (any, gperr.Error) {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ This package provides robust YAML/JSON serialization with:
|
|||||||
- Case-insensitive field matching using FNV-1a hashing
|
- Case-insensitive field matching using FNV-1a hashing
|
||||||
- Environment variable substitution (`${VAR}` syntax)
|
- Environment variable substitution (`${VAR}` syntax)
|
||||||
- Field-level validation with go-playground/validator tags
|
- Field-level validation with go-playground/validator tags
|
||||||
- Custom type conversion with alias support
|
- Custom type conversion with pluggable format handlers
|
||||||
|
|
||||||
### Primary Consumers
|
### Primary Consumers
|
||||||
|
|
||||||
@@ -55,21 +55,27 @@ type CustomValidator interface {
|
|||||||
### Deserialization Functions
|
### Deserialization Functions
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// YAML with full validation
|
// Generic unmarshal with pluggable format handler
|
||||||
func UnmarshalValidateYAML[T any](data []byte, target *T) gperr.Error
|
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) gperr.Error
|
||||||
|
|
||||||
// YAML with interceptor for preprocessing
|
// Read from io.Reader with format decoder
|
||||||
func UnmarshalValidateYAMLIntercept[T any](
|
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) gperr.Error
|
||||||
data []byte,
|
|
||||||
target *T,
|
|
||||||
intercept func(m map[string]any) gperr.Error,
|
|
||||||
) gperr.Error
|
|
||||||
|
|
||||||
// Direct map deserialization
|
// Direct map deserialization
|
||||||
func MapUnmarshalValidate(src SerializedObject, dst any) gperr.Error
|
func MapUnmarshalValidate(src SerializedObject, dst any) gperr.Error
|
||||||
|
|
||||||
// To xsync.Map
|
// To xsync.Map with pluggable format handler
|
||||||
func UnmarshalValidateYAMLXSync[V any](data []byte) (*xsync.Map[string, V], gperr.Error)
|
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], gperr.Error)
|
||||||
|
```
|
||||||
|
|
||||||
|
### File I/O Functions
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Write marshaled data to file
|
||||||
|
func SaveFile[T any](path string, src *T, perm os.FileMode, marshaler marshalFunc) error
|
||||||
|
|
||||||
|
// Read and unmarshal file if it exists
|
||||||
|
func LoadFileIfExist[T any](path string, dst *T, unmarshaler unmarshalFunc) error
|
||||||
```
|
```
|
||||||
|
|
||||||
### Conversion Functions
|
### Conversion Functions
|
||||||
@@ -115,19 +121,19 @@ func ToSerializedObject[VT any](m map[string]VT) SerializedObject
|
|||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant C as Caller
|
participant C as Caller
|
||||||
participant U as UnmarshalValidateYAML
|
participant U as UnmarshalValidate
|
||||||
participant E as Env Substitution
|
participant E as Env Substitution
|
||||||
participant Y as YAML Parser
|
participant F as Format Parser
|
||||||
participant M as MapUnmarshalValidate
|
participant M as MapUnmarshalValidate
|
||||||
participant T as Type Info Cache
|
participant T as Type Info Cache
|
||||||
participant CV as Convert
|
participant CV as Convert
|
||||||
participant V as Validator
|
participant V as Validator
|
||||||
|
|
||||||
C->>U: YAML bytes + target struct
|
C->>U: Data bytes + target struct + format handler
|
||||||
U->>E: Substitute ${ENV} vars
|
U->>E: Substitute ${ENV} vars
|
||||||
E-->>U: Substituted bytes
|
E-->>U: Substituted bytes
|
||||||
U->>Y: Parse YAML
|
U->>F: Parse with format handler (YAML/JSON)
|
||||||
Y-->>U: map[string]any
|
F-->>U: map[string]any
|
||||||
U->>M: Map + target
|
U->>M: Map + target
|
||||||
M->>T: Get type info
|
M->>T: Get type info
|
||||||
loop For each field in map
|
loop For each field in map
|
||||||
@@ -147,9 +153,9 @@ sequenceDiagram
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
subgraph Input Processing
|
subgraph Input Processing
|
||||||
YAML[YAML Bytes] --> EnvSub[Env Substitution]
|
Bytes[Data Bytes] --> EnvSub[Env Substitution]
|
||||||
EnvSub --> YAMLParse[YAML Parse]
|
EnvSub --> FormatParse[Format Parse]
|
||||||
YAMLParse --> Map[map<string,any>]
|
FormatParse --> Map[map<string,any>]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Type Inspection
|
subgraph Type Inspection
|
||||||
@@ -221,6 +227,7 @@ autocert:
|
|||||||
### Internal Dependencies
|
### Internal Dependencies
|
||||||
|
|
||||||
- `github.com/yusing/goutils/errs` - Error handling
|
- `github.com/yusing/goutils/errs` - Error handling
|
||||||
|
- `github.com/yusing/gointernals` - Reflection utilities
|
||||||
|
|
||||||
## Observability
|
## Observability
|
||||||
|
|
||||||
@@ -251,11 +258,11 @@ ErrUnsupportedConversion.Subjectf("string to int")
|
|||||||
| Validation failure | Structured error | Fix field value |
|
| Validation failure | Structured error | Fix field value |
|
||||||
| Type mismatch | Error | Check field type |
|
| Type mismatch | Error | Check field type |
|
||||||
| Missing env var | Error | Set environment variable |
|
| Missing env var | Error | Set environment variable |
|
||||||
| Invalid YAML | Error | Fix YAML syntax |
|
| Invalid format | Error | Fix YAML/JSON syntax |
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Struct Deserialization
|
### YAML Deserialization
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -273,7 +280,16 @@ tls_enabled: true
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
var config ServerConfig
|
var config ServerConfig
|
||||||
if err := serialization.UnmarshalValidateYAML(yamlData, &config); err != nil {
|
if err := serialization.UnmarshalValidate(yamlData, &config, yaml.Unmarshal); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Deserialization
|
||||||
|
|
||||||
|
```go
|
||||||
|
var config ServerConfig
|
||||||
|
if err := serialization.UnmarshalValidate(jsonData, &config, json.Unmarshal); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -293,7 +309,7 @@ func (c *Config) Validate() gperr.Error {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Type with Parse Method
|
### Custom Type with Parser Interface
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Duration struct {
|
type Duration struct {
|
||||||
@@ -307,6 +323,31 @@ func (d *Duration) Parse(v string) error {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Reading from File
|
||||||
|
|
||||||
|
```go
|
||||||
|
var config ServerConfig
|
||||||
|
if err := serialization.LoadFileIfExist("config.yml", &config, yaml.Unmarshal); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save back to file
|
||||||
|
if err := serialization.SaveFile("config.yml", &config, 0644, yaml.Marshal); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading from io.Reader
|
||||||
|
|
||||||
|
```go
|
||||||
|
var config ServerConfig
|
||||||
|
file, _ := os.Open("config.yml")
|
||||||
|
defer file.Close()
|
||||||
|
if err := serialization.UnmarshalValidateReader(file, &config, yaml.NewDecoder); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Testing Notes
|
## Testing Notes
|
||||||
|
|
||||||
- `serialization_test.go` - Core functionality tests
|
- `serialization_test.go` - Core functionality tests
|
||||||
@@ -319,3 +360,4 @@ func (d *Duration) Parse(v string) error {
|
|||||||
- String conversions
|
- String conversions
|
||||||
- Environment substitution
|
- Environment substitution
|
||||||
- Custom validators
|
- Custom validators
|
||||||
|
- Multiple format handlers (YAML/JSON)
|
||||||
|
|||||||
37
internal/serialization/gin_binding.go
Normal file
37
internal/serialization/gin_binding.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package serialization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
GinJSONBinding struct{}
|
||||||
|
GinYAMLBinding struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b GinJSONBinding) Name() string {
|
||||||
|
return "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b GinJSONBinding) Bind(req *http.Request, obj any) error {
|
||||||
|
m := make(map[string]any)
|
||||||
|
if err := sonic.ConfigDefault.NewDecoder(NewSubstituteEnvReader(req.Body)).Decode(&m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return MapUnmarshalValidate(m, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b GinYAMLBinding) Name() string {
|
||||||
|
return "yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b GinYAMLBinding) Bind(req *http.Request, obj any) error {
|
||||||
|
m := make(map[string]any)
|
||||||
|
if err := yaml.NewDecoder(NewSubstituteEnvReader(req.Body)).Decode(&m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return MapUnmarshalValidate(m, obj)
|
||||||
|
}
|
||||||
50
internal/serialization/gin_binding_test.go
Normal file
50
internal/serialization/gin_binding_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package serialization_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestStruct struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Value2 int `json:"value2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TestStruct) Validate() gperr.Error {
|
||||||
|
if t.Value == "" {
|
||||||
|
return gperr.New("value is required")
|
||||||
|
}
|
||||||
|
if t.Value2 != 0 && (t.Value2 < 5 || t.Value2 > 10) {
|
||||||
|
return gperr.New("value2 must be between 5 and 10")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGinBinding(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"valid1", `{"value": "test", "value2": 7}`, false},
|
||||||
|
{"valid2", `{"value": "test"}`, false},
|
||||||
|
{"invalid1", `{"value2": 7}`, true},
|
||||||
|
{"invalid2", `{"value": "test", "value2": 3}`, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var dst TestStruct
|
||||||
|
body := bytes.NewBufferString(tt.input)
|
||||||
|
req := httptest.NewRequest("POST", "/", body)
|
||||||
|
err := serialization.GinJSONBinding{}.Bind(req, &dst)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("%s: Bind() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
146
internal/serialization/reader.go
Normal file
146
internal/serialization/reader.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package serialization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubstituteEnvReader struct {
|
||||||
|
reader io.Reader
|
||||||
|
buf []byte // buffered data with substitutions applied
|
||||||
|
err error // sticky error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubstituteEnvReader(reader io.Reader) *SubstituteEnvReader {
|
||||||
|
return &SubstituteEnvReader{reader: reader}
|
||||||
|
}
|
||||||
|
|
||||||
|
const peekSize = 4096
|
||||||
|
const maxVarNameLength = 256
|
||||||
|
|
||||||
|
func (r *SubstituteEnvReader) Read(p []byte) (n int, err error) {
|
||||||
|
// Return buffered data first
|
||||||
|
if len(r.buf) > 0 {
|
||||||
|
n = copy(p, r.buf)
|
||||||
|
r.buf = r.buf[n:]
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return sticky error if we have one
|
||||||
|
if r.err != nil {
|
||||||
|
return 0, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [2 * peekSize]byte
|
||||||
|
|
||||||
|
// Read a chunk from the underlying reader
|
||||||
|
chunk, more := buf[:peekSize], buf[peekSize:]
|
||||||
|
nRead, readErr := r.reader.Read(chunk)
|
||||||
|
if nRead == 0 {
|
||||||
|
if readErr != nil {
|
||||||
|
return 0, readErr
|
||||||
|
}
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
chunk = chunk[:nRead]
|
||||||
|
|
||||||
|
// Check if there's a potential incomplete pattern at the end
|
||||||
|
// Pattern: ${VAR_NAME}
|
||||||
|
// We need to check if chunk ends with a partial pattern like "$", "${", "${VAR", etc.
|
||||||
|
incompleteStart := findIncompletePatternStart(chunk)
|
||||||
|
|
||||||
|
if incompleteStart >= 0 && readErr == nil {
|
||||||
|
// There might be an incomplete pattern, read more to complete it
|
||||||
|
incomplete := chunk[incompleteStart:]
|
||||||
|
chunk = chunk[:incompleteStart]
|
||||||
|
|
||||||
|
// Keep reading until we complete the pattern or hit EOF/error
|
||||||
|
for {
|
||||||
|
// Limit how much we buffer to prevent memory exhaustion
|
||||||
|
if len(incomplete) > maxVarNameLength+3 { // ${} + var name
|
||||||
|
// Pattern too long to be valid, give up and process as-is
|
||||||
|
chunk = append(chunk, incomplete...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nMore, moreErr := r.reader.Read(more)
|
||||||
|
if nMore > 0 {
|
||||||
|
incomplete = append(incomplete, more[:nMore]...)
|
||||||
|
// Check if pattern is now complete
|
||||||
|
if idx := bytes.IndexByte(incomplete, '}'); idx >= 0 {
|
||||||
|
// Pattern complete, append the rest back to chunk
|
||||||
|
chunk = append(chunk, incomplete...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if moreErr != nil {
|
||||||
|
// No more data, append whatever we have
|
||||||
|
chunk = append(chunk, incomplete...)
|
||||||
|
readErr = moreErr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
substituted, subErr := substituteEnv(chunk)
|
||||||
|
if subErr != nil {
|
||||||
|
r.err = subErr
|
||||||
|
return 0, subErr
|
||||||
|
}
|
||||||
|
|
||||||
|
n = copy(p, substituted)
|
||||||
|
if n < len(substituted) {
|
||||||
|
// Buffer the rest
|
||||||
|
r.buf = substituted[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store sticky error for next read
|
||||||
|
if readErr != nil && readErr != io.EOF {
|
||||||
|
r.err = readErr
|
||||||
|
} else {
|
||||||
|
if readErr == io.EOF && n > 0 {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
if readErr == io.EOF {
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findIncompletePatternStart returns the index where an incomplete ${...} pattern starts,
|
||||||
|
// or -1 if there's no incomplete pattern at the end.
|
||||||
|
func findIncompletePatternStart(data []byte) int {
|
||||||
|
// Look for '$' near the end that might be start of ${VAR}
|
||||||
|
// Maximum var name we reasonably expect + "${}" = ~256 chars
|
||||||
|
searchStart := max(0, len(data)-maxVarNameLength)
|
||||||
|
|
||||||
|
for i := len(data) - 1; i >= searchStart; i-- {
|
||||||
|
if data[i] == '$' {
|
||||||
|
// Check if this is a complete pattern or incomplete
|
||||||
|
if i+1 >= len(data) {
|
||||||
|
// Just "$" at end
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
if data[i+1] == '{' {
|
||||||
|
// Check if there's anything after "${"
|
||||||
|
if i+2 >= len(data) {
|
||||||
|
// Just "${" at end
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
// Check if pattern is complete by looking for '}'
|
||||||
|
for j := i + 2; j < len(data); j++ {
|
||||||
|
if data[j] == '}' {
|
||||||
|
// This pattern is complete, continue searching for another
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if j == len(data)-1 {
|
||||||
|
// Reached end without finding '}', incomplete pattern
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
286
internal/serialization/reader_bench_test.go
Normal file
286
internal/serialization/reader_bench_test.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
package serialization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupEnv sets up environment variables for benchmarks
|
||||||
|
func setupEnv(b *testing.B) {
|
||||||
|
b.Helper()
|
||||||
|
os.Setenv("BENCH_VAR", "benchmark_value")
|
||||||
|
os.Setenv("BENCH_VAR_2", "second_value")
|
||||||
|
os.Setenv("BENCH_VAR_3", "third_value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupEnv cleans up environment variables after benchmarks
|
||||||
|
func cleanupEnv(b *testing.B) {
|
||||||
|
b.Helper()
|
||||||
|
os.Unsetenv("BENCH_VAR")
|
||||||
|
os.Unsetenv("BENCH_VAR_2")
|
||||||
|
os.Unsetenv("BENCH_VAR_3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_NoSubstitution benchmarks reading without any env substitutions
|
||||||
|
func BenchmarkSubstituteEnvReader_NoSubstitution(b *testing.B) {
|
||||||
|
r := strings.NewReader(`key: value
|
||||||
|
name: test
|
||||||
|
data: some content here
|
||||||
|
`)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_SingleSubstitution benchmarks reading with a single env substitution
|
||||||
|
func BenchmarkSubstituteEnvReader_SingleSubstitution(b *testing.B) {
|
||||||
|
setupEnv(b)
|
||||||
|
defer cleanupEnv(b)
|
||||||
|
|
||||||
|
r := strings.NewReader(`key: ${BENCH_VAR}
|
||||||
|
`)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_MultipleSubstitutions benchmarks reading with multiple env substitutions
|
||||||
|
func BenchmarkSubstituteEnvReader_MultipleSubstitutions(b *testing.B) {
|
||||||
|
setupEnv(b)
|
||||||
|
defer cleanupEnv(b)
|
||||||
|
|
||||||
|
r := strings.NewReader(`key1: ${BENCH_VAR}
|
||||||
|
key2: ${BENCH_VAR_2}
|
||||||
|
key3: ${BENCH_VAR_3}
|
||||||
|
`)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_LargeInput_NoSubstitution benchmarks large input without substitutions
|
||||||
|
func BenchmarkSubstituteEnvReader_LargeInput_NoSubstitution(b *testing.B) {
|
||||||
|
r := strings.NewReader(strings.Repeat("x", 100000))
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_LargeInput_WithSubstitutions benchmarks large input with scattered substitutions
|
||||||
|
func BenchmarkSubstituteEnvReader_LargeInput_WithSubstitutions(b *testing.B) {
|
||||||
|
setupEnv(b)
|
||||||
|
defer cleanupEnv(b)
|
||||||
|
|
||||||
|
var builder bytes.Buffer
|
||||||
|
for range 100 {
|
||||||
|
builder.WriteString(strings.Repeat("x", 1000))
|
||||||
|
builder.WriteString("${BENCH_VAR}")
|
||||||
|
}
|
||||||
|
r := bytes.NewReader(builder.Bytes())
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_SmallBuffer benchmarks reading with a small buffer size
|
||||||
|
func BenchmarkSubstituteEnvReader_SmallBuffer(b *testing.B) {
|
||||||
|
setupEnv(b)
|
||||||
|
defer cleanupEnv(b)
|
||||||
|
|
||||||
|
r := strings.NewReader(`key: ${BENCH_VAR} and some more content here`)
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
for {
|
||||||
|
_, err := reader.Read(buf)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_YAMLConfig benchmarks a realistic YAML config scenario
|
||||||
|
func BenchmarkSubstituteEnvReader_YAMLConfig(b *testing.B) {
|
||||||
|
setupEnv(b)
|
||||||
|
defer cleanupEnv(b)
|
||||||
|
|
||||||
|
r := strings.NewReader(`database:
|
||||||
|
host: ${BENCH_VAR}
|
||||||
|
port: ${BENCH_VAR_2}
|
||||||
|
username: ${BENCH_VAR_3}
|
||||||
|
password: ${BENCH_VAR}
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
ttl: ${BENCH_VAR_2}
|
||||||
|
server:
|
||||||
|
host: ${BENCH_VAR}
|
||||||
|
port: 8080
|
||||||
|
`)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_BoundaryPattern benchmarks patterns at buffer boundaries (4096 bytes)
|
||||||
|
func BenchmarkSubstituteEnvReader_BoundaryPattern(b *testing.B) {
|
||||||
|
setupEnv(b)
|
||||||
|
defer cleanupEnv(b)
|
||||||
|
|
||||||
|
// Pattern exactly at 4090 bytes, with ${VAR} crossing the 4096 boundary
|
||||||
|
prefix := strings.Repeat("x", 4090)
|
||||||
|
r := strings.NewReader(prefix + "${BENCH_VAR}")
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_MultipleBoundaries benchmarks multiple patterns crossing boundaries
|
||||||
|
func BenchmarkSubstituteEnvReader_MultipleBoundaries(b *testing.B) {
|
||||||
|
setupEnv(b)
|
||||||
|
defer cleanupEnv(b)
|
||||||
|
|
||||||
|
var builder bytes.Buffer
|
||||||
|
for range 10 {
|
||||||
|
builder.WriteString(strings.Repeat("x", 4000))
|
||||||
|
builder.WriteString("${BENCH_VAR}")
|
||||||
|
}
|
||||||
|
r := bytes.NewReader(builder.Bytes())
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_SpecialChars benchmarks substitution with special characters
|
||||||
|
func BenchmarkSubstituteEnvReader_SpecialChars(b *testing.B) {
|
||||||
|
os.Setenv("SPECIAL_BENCH_VAR", `value with "quotes" and \backslash\`)
|
||||||
|
defer os.Unsetenv("SPECIAL_BENCH_VAR")
|
||||||
|
|
||||||
|
r := strings.NewReader(`key: ${SPECIAL_BENCH_VAR}
|
||||||
|
`)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_EmptyValue benchmarks substitution with empty value
|
||||||
|
func BenchmarkSubstituteEnvReader_EmptyValue(b *testing.B) {
|
||||||
|
os.Setenv("EMPTY_BENCH_VAR", "")
|
||||||
|
defer os.Unsetenv("EMPTY_BENCH_VAR")
|
||||||
|
|
||||||
|
r := strings.NewReader(`key: ${EMPTY_BENCH_VAR}
|
||||||
|
`)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSubstituteEnvReader_DollarWithoutBrace benchmarks $ without following {
|
||||||
|
func BenchmarkSubstituteEnvReader_DollarWithoutBrace(b *testing.B) {
|
||||||
|
os.Setenv("BENCH_VAR", "benchmark_value")
|
||||||
|
defer os.Unsetenv("BENCH_VAR")
|
||||||
|
|
||||||
|
r := strings.NewReader(`price: $100 and $200 for ${BENCH_VAR}`)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
reader := NewSubstituteEnvReader(r)
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Seek(0, io.SeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkFindIncompletePatternStart benchmarks the findIncompletePatternStart function
|
||||||
|
func BenchmarkFindIncompletePatternStart(b *testing.B) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{"no pattern", strings.Repeat("hello world ", 100)},
|
||||||
|
{"complete pattern", strings.Repeat("hello ${VAR} world ", 50)},
|
||||||
|
{"dollar at end", strings.Repeat("hello ", 100) + "$"},
|
||||||
|
{"incomplete at end", strings.Repeat("hello ", 100) + "${VAR"},
|
||||||
|
{"large input no pattern", strings.Repeat("x", 5000)},
|
||||||
|
{"large input with pattern", strings.Repeat("x", 4000) + "${VAR}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
b.Run(tc.name, func(b *testing.B) {
|
||||||
|
data := []byte(tc.input)
|
||||||
|
for b.Loop() {
|
||||||
|
findIncompletePatternStart(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
217
internal/serialization/reader_test.go
Normal file
217
internal/serialization/reader_test.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package serialization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_Basic(t *testing.T) {
|
||||||
|
os.Setenv("TEST_VAR", "hello")
|
||||||
|
defer os.Unsetenv("TEST_VAR")
|
||||||
|
|
||||||
|
input := []byte(`key: ${TEST_VAR}`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, `key: "hello"`, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_Multiple(t *testing.T) {
|
||||||
|
os.Setenv("VAR1", "first")
|
||||||
|
os.Setenv("VAR2", "second")
|
||||||
|
defer os.Unsetenv("VAR1")
|
||||||
|
defer os.Unsetenv("VAR2")
|
||||||
|
|
||||||
|
input := []byte(`a: ${VAR1}, b: ${VAR2}`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, `a: "first", b: "second"`, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_NoSubstitution(t *testing.T) {
|
||||||
|
input := []byte(`key: value`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, `key: value`, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_UnsetEnvError(t *testing.T) {
|
||||||
|
os.Unsetenv("UNSET_VAR_FOR_TEST")
|
||||||
|
|
||||||
|
input := []byte(`key: ${UNSET_VAR_FOR_TEST}`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
_, err := io.ReadAll(reader)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "UNSET_VAR_FOR_TEST is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_SmallBuffer(t *testing.T) {
|
||||||
|
os.Setenv("SMALL_BUF_VAR", "value")
|
||||||
|
defer os.Unsetenv("SMALL_BUF_VAR")
|
||||||
|
|
||||||
|
input := []byte(`key: ${SMALL_BUF_VAR}`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
var result []byte
|
||||||
|
buf := make([]byte, 3)
|
||||||
|
for {
|
||||||
|
n, err := reader.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
result = append(result, buf[:n]...)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, `key: "value"`, string(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_SpecialChars(t *testing.T) {
|
||||||
|
os.Setenv("SPECIAL_VAR", `hello "world" \n`)
|
||||||
|
defer os.Unsetenv("SPECIAL_VAR")
|
||||||
|
|
||||||
|
input := []byte(`key: ${SPECIAL_VAR}`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, `key: "hello \"world\" \\n"`, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_EmptyValue(t *testing.T) {
|
||||||
|
os.Setenv("EMPTY_VAR", "")
|
||||||
|
defer os.Unsetenv("EMPTY_VAR")
|
||||||
|
|
||||||
|
input := []byte(`key: ${EMPTY_VAR}`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, `key: ""`, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_LargeInput(t *testing.T) {
|
||||||
|
os.Setenv("LARGE_VAR", "replaced")
|
||||||
|
defer os.Unsetenv("LARGE_VAR")
|
||||||
|
|
||||||
|
prefix := strings.Repeat("x", 5000)
|
||||||
|
suffix := strings.Repeat("y", 5000)
|
||||||
|
input := []byte(prefix + "${LARGE_VAR}" + suffix)
|
||||||
|
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := prefix + `"replaced"` + suffix
|
||||||
|
require.Equal(t, expected, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_PatternAtBoundary(t *testing.T) {
|
||||||
|
os.Setenv("BOUNDARY_VAR", "boundary_value")
|
||||||
|
defer os.Unsetenv("BOUNDARY_VAR")
|
||||||
|
|
||||||
|
prefix := strings.Repeat("a", 4090)
|
||||||
|
input := []byte(prefix + "${BOUNDARY_VAR}")
|
||||||
|
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := prefix + `"boundary_value"`
|
||||||
|
require.Equal(t, expected, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_MultiplePatternsBoundary(t *testing.T) {
|
||||||
|
os.Setenv("VAR_A", "aaa")
|
||||||
|
os.Setenv("VAR_B", "bbb")
|
||||||
|
defer os.Unsetenv("VAR_A")
|
||||||
|
defer os.Unsetenv("VAR_B")
|
||||||
|
|
||||||
|
prefix := strings.Repeat("x", 4090)
|
||||||
|
input := []byte(prefix + "${VAR_A} middle ${VAR_B}")
|
||||||
|
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := prefix + `"aaa" middle "bbb"`
|
||||||
|
require.Equal(t, expected, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_YAMLConfig(t *testing.T) {
|
||||||
|
os.Setenv("DB_HOST", "localhost")
|
||||||
|
os.Setenv("DB_PORT", "5432")
|
||||||
|
os.Setenv("DB_PASSWORD", "secret123")
|
||||||
|
defer os.Unsetenv("DB_HOST")
|
||||||
|
defer os.Unsetenv("DB_PORT")
|
||||||
|
defer os.Unsetenv("DB_PASSWORD")
|
||||||
|
|
||||||
|
input := []byte(`database:
|
||||||
|
host: ${DB_HOST}
|
||||||
|
port: ${DB_PORT}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := `database:
|
||||||
|
host: "localhost"
|
||||||
|
port: "5432"
|
||||||
|
password: "secret123"
|
||||||
|
`
|
||||||
|
require.Equal(t, expected, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_DollarWithoutBrace(t *testing.T) {
|
||||||
|
input := []byte(`key: $NOT_A_PATTERN`)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, `key: $NOT_A_PATTERN`, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEnvReader_EmptyInput(t *testing.T) {
|
||||||
|
input := []byte(``)
|
||||||
|
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||||
|
|
||||||
|
output, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ``, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindIncompletePatternStart(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"no pattern", "hello world", -1},
|
||||||
|
{"complete pattern", "hello ${VAR} world", -1},
|
||||||
|
{"dollar at end", "hello $", 6},
|
||||||
|
{"dollar brace at end", "hello ${", 6},
|
||||||
|
{"incomplete var at end", "hello ${VAR", 6},
|
||||||
|
{"complete then incomplete", "hello ${VAR} ${INCOMPLETE", 13},
|
||||||
|
{"multiple complete", "${A} ${B} ${C}", -1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := findIncompletePatternStart([]byte(tt.input))
|
||||||
|
require.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package serialization
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -85,6 +86,10 @@ func initPtr(dst reflect.Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate performs struct validation using go-playground/validator tags.
|
||||||
|
//
|
||||||
|
// It collects all validation errors and returns them as a single error.
|
||||||
|
// Field names in errors are prefixed with their namespace (e.g., "User.Email").
|
||||||
func ValidateWithFieldTags(s any) gperr.Error {
|
func ValidateWithFieldTags(s any) gperr.Error {
|
||||||
var errs gperr.Builder
|
var errs gperr.Builder
|
||||||
err := validate.Struct(s)
|
err := validate.Struct(s)
|
||||||
@@ -257,10 +262,11 @@ func initTypeKeyFieldIndexesMap(t reflect.Type) typeInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapUnmarshalValidate takes a SerializedObject and a target value, and assigns the values in the SerializedObject to the target value.
|
// MapUnmarshalValidate takes a SerializedObject and a target value,
|
||||||
// MapUnmarshalValidate ignores case differences between the field names in the SerializedObject and the target.
|
// and assigns the values in the SerializedObject to the target value.
|
||||||
|
//
|
||||||
|
// It ignores case differences between the field names in the SerializedObject and the target.
|
||||||
//
|
//
|
||||||
// The target value must be a struct or a map[string]any.
|
|
||||||
// If the target value is a struct , and implements the MapUnmarshaller interface,
|
// If the target value is a struct , and implements the MapUnmarshaller interface,
|
||||||
// the UnmarshalMap method will be called.
|
// the UnmarshalMap method will be called.
|
||||||
//
|
//
|
||||||
@@ -309,7 +315,7 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
|
|||||||
info := getTypeInfo(dstT)
|
info := getTypeInfo(dstT)
|
||||||
for k, v := range src {
|
for k, v := range src {
|
||||||
if field, ok := info.getField(dstV, k); ok {
|
if field, ok := info.getField(dstV, k); ok {
|
||||||
err := Convert(reflect.ValueOf(v), field, !info.hasValidateTag)
|
err := Convert(reflect.ValueOf(v), field, checkValidateTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(err.Subject(k))
|
errs.Add(err.Subject(k))
|
||||||
}
|
}
|
||||||
@@ -455,6 +461,13 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
|
|||||||
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
|
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConvertSlice converts a source slice to a destination slice.
|
||||||
|
//
|
||||||
|
// - Elements are converted one by one using the Convert function.
|
||||||
|
// - Validation is performed on each element if checkValidateTag is true.
|
||||||
|
// - The destination slice is initialized with the source length.
|
||||||
|
// - On error, the destination slice is truncated to the number of
|
||||||
|
// successfully converted elements.
|
||||||
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
|
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
|
||||||
if dst.Kind() == reflect.Pointer {
|
if dst.Kind() == reflect.Pointer {
|
||||||
if dst.IsNil() && !dst.CanSet() {
|
if dst.IsNil() && !dst.CanSet() {
|
||||||
@@ -507,6 +520,12 @@ func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) g
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConvertString converts a string value to the destination reflect.Value.
|
||||||
|
// - It handles various types including numeric types, booleans, time.Duration,
|
||||||
|
// slices (comma-separated or YAML), maps, and structs (YAML).
|
||||||
|
// - If the destination implements the Parser interface, it is used for conversion.
|
||||||
|
// - Returns true if conversion was handled (even with error), false if
|
||||||
|
// conversion is unsupported.
|
||||||
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
|
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
|
||||||
convertible = true
|
convertible = true
|
||||||
dstT := dst.Type()
|
dstT := dst.Type()
|
||||||
@@ -618,48 +637,80 @@ func substituteEnv(data []byte) ([]byte, gperr.Error) {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnmarshalValidateYAML[T any](data []byte, target *T) gperr.Error {
|
type (
|
||||||
|
marshalFunc func(src any) ([]byte, error)
|
||||||
|
unmarshalFunc func(data []byte, target any) error
|
||||||
|
newDecoderFunc func(r io.Reader) interface {
|
||||||
|
Decode(v any) error
|
||||||
|
}
|
||||||
|
interceptFunc func(m map[string]any) gperr.Error
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnmarshalValidate unmarshals data into a map, applies optional intercept
|
||||||
|
// functions, and validates the result against the target struct using field tags.
|
||||||
|
// - Environment variables in the data are substituted using ${VAR} syntax.
|
||||||
|
// - The unmarshaler function converts data to a map[string]any.
|
||||||
|
// - Intercept functions can modify or validate the map before unmarshaling.
|
||||||
|
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) gperr.Error {
|
||||||
data, err := substituteEnv(data)
|
data, err := substituteEnv(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := make(map[string]any)
|
m := make(map[string]any)
|
||||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
if err := unmarshaler(data, &m); err != nil {
|
||||||
return gperr.Wrap(err)
|
return gperr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
for _, intercept := range interceptFns {
|
||||||
|
if err := intercept(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return MapUnmarshalValidate(m, target)
|
return MapUnmarshalValidate(m, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnmarshalValidateYAMLIntercept[T any](data []byte, target *T, intercept func(m map[string]any) gperr.Error) gperr.Error {
|
// UnmarshalValidateReader reads from an io.Reader, unmarshals to a map,
|
||||||
|
// - Applies optional intercept functions, and validates against the target struct.
|
||||||
|
// - Environment variables are substituted during reading using ${VAR} syntax.
|
||||||
|
// - The newDecoder function creates a decoder for the reader (e.g.,
|
||||||
|
// json.NewDecoder).
|
||||||
|
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) gperr.Error {
|
||||||
|
m := make(map[string]any)
|
||||||
|
if err := newDecoder(NewSubstituteEnvReader(reader)).Decode(&m); err != nil {
|
||||||
|
return gperr.Wrap(err)
|
||||||
|
}
|
||||||
|
for _, intercept := range interceptFns {
|
||||||
|
if err := intercept(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MapUnmarshalValidate(m, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalValidateXSync unmarshals data into an xsync.Map[string, V].
|
||||||
|
// - Environment variables in the data are substituted using ${VAR} syntax.
|
||||||
|
// - The unmarshaler function converts data to a map[string]any.
|
||||||
|
// - Intercept functions can modify or validate the map before unmarshaling.
|
||||||
|
// - Returns a thread-safe concurrent map with the unmarshaled values.
|
||||||
|
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], gperr.Error) {
|
||||||
data, err := substituteEnv(data)
|
data, err := substituteEnv(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := make(map[string]any)
|
m := make(map[string]any)
|
||||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
if err := unmarshaler(data, &m); err != nil {
|
||||||
return gperr.Wrap(err)
|
return nil, gperr.Wrap(err)
|
||||||
}
|
}
|
||||||
if err := intercept(m); err != nil {
|
for _, intercept := range interceptFns {
|
||||||
return err
|
if err := intercept(m); err != nil {
|
||||||
}
|
return nil, err
|
||||||
return MapUnmarshalValidate(m, target)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], err gperr.Error) {
|
|
||||||
data, err = substituteEnv(data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m := make(map[string]any)
|
|
||||||
if err = gperr.Wrap(yaml.Unmarshal(data, &m)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m2 := make(map[string]V, len(m))
|
m2 := make(map[string]V, len(m))
|
||||||
if err = MapUnmarshalValidate(m, m2); err != nil {
|
if err = MapUnmarshalValidate(m, m2); err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
ret := xsync.NewMap[string, V](xsync.WithPresize(len(m)))
|
ret := xsync.NewMap[string, V](xsync.WithPresize(len(m)))
|
||||||
for k, v := range m2 {
|
for k, v := range m2 {
|
||||||
@@ -668,26 +719,27 @@ func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], er
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSerialized[T any](path string, dst *T, deserialize func(data []byte, dst any) error) error {
|
// SaveFile marshals a value to bytes and writes it to a file.
|
||||||
data, err := os.ReadFile(path)
|
// - The marshaler function converts the value to bytes.
|
||||||
if err != nil {
|
// - The file is written with the specified permissions.
|
||||||
return err
|
func SaveFile[T any](path string, src *T, perm os.FileMode, marshaler marshalFunc) error {
|
||||||
}
|
data, err := marshaler(src)
|
||||||
return deserialize(data, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveJSON[T any](path string, src *T, perm os.FileMode) error {
|
|
||||||
data, err := sonic.Marshal(src)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, data, perm)
|
return os.WriteFile(path, data, perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadJSONIfExist[T any](path string, dst *T) error {
|
// LoadFileIfExist reads a file and unmarshals its contents to a value.
|
||||||
err := loadSerialized(path, dst, sonic.Unmarshal)
|
// - The unmarshaler function converts the bytes to a value.
|
||||||
if os.IsNotExist(err) {
|
// - If the file does not exist, nil is returned and dst remains unchanged.
|
||||||
return nil
|
func LoadFileIfExist[T any](path string, dst *T, unmarshaler unmarshalFunc) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return err
|
return unmarshaler(data, dst)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
expect "github.com/yusing/goutils/testing"
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
@@ -303,6 +304,6 @@ autocert:
|
|||||||
} `yaml:"options"`
|
} `yaml:"options"`
|
||||||
} `yaml:"autocert"`
|
} `yaml:"autocert"`
|
||||||
}
|
}
|
||||||
require.NoError(t, UnmarshalValidateYAML(data, &cfg))
|
require.NoError(t, UnmarshalValidate(data, &cfg, yaml.Unmarshal))
|
||||||
require.Equal(t, "test", cfg.Autocert.Options.AuthToken)
|
require.Equal(t, "test", cfg.Autocert.Options.AuthToken)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,17 +29,19 @@ type CustomValidator interface {
|
|||||||
var validatorType = reflect.TypeFor[CustomValidator]()
|
var validatorType = reflect.TypeFor[CustomValidator]()
|
||||||
|
|
||||||
func ValidateWithCustomValidator(v reflect.Value) gperr.Error {
|
func ValidateWithCustomValidator(v reflect.Value) gperr.Error {
|
||||||
|
vt := v.Type()
|
||||||
if v.Kind() == reflect.Pointer {
|
if v.Kind() == reflect.Pointer {
|
||||||
if v.IsNil() {
|
elemType := vt.Elem()
|
||||||
// return nil
|
if vt.Implements(validatorType) {
|
||||||
return validateWithValidator(reflect.New(v.Type().Elem()))
|
if v.IsNil() {
|
||||||
}
|
return reflect.New(elemType).Interface().(CustomValidator).Validate()
|
||||||
if v.Type().Implements(validatorType) {
|
}
|
||||||
return v.Interface().(CustomValidator).Validate()
|
return v.Interface().(CustomValidator).Validate()
|
||||||
}
|
}
|
||||||
return validateWithValidator(v.Elem())
|
if elemType.Implements(validatorType) {
|
||||||
|
return v.Elem().Interface().(CustomValidator).Validate()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
vt := v.Type()
|
|
||||||
if vt.PkgPath() != "" { // not a builtin type
|
if vt.PkgPath() != "" { // not a builtin type
|
||||||
// prioritize pointer method
|
// prioritize pointer method
|
||||||
if v.CanAddr() {
|
if v.CanAddr() {
|
||||||
@@ -56,10 +58,3 @@ func ValidateWithCustomValidator(v reflect.Value) gperr.Error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateWithValidator(v reflect.Value) gperr.Error {
|
|
||||||
if v.Type().Implements(validatorType) {
|
|
||||||
return v.Interface().(CustomValidator).Validate()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package types
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
)
|
)
|
||||||
@@ -10,14 +11,14 @@ import (
|
|||||||
func TestDockerProviderConfigUnmarshalMap(t *testing.T) {
|
func TestDockerProviderConfigUnmarshalMap(t *testing.T) {
|
||||||
t.Run("string", func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
var cfg map[string]*DockerProviderConfig
|
var cfg map[string]*DockerProviderConfig
|
||||||
err := serialization.UnmarshalValidateYAML([]byte("test: http://localhost:2375"), &cfg)
|
err := serialization.UnmarshalValidate([]byte("test: http://localhost:2375"), &cfg, yaml.Unmarshal)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375"}, cfg["test"])
|
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375"}, cfg["test"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("detailed", func(t *testing.T) {
|
t.Run("detailed", func(t *testing.T) {
|
||||||
var cfg map[string]*DockerProviderConfig
|
var cfg map[string]*DockerProviderConfig
|
||||||
err := serialization.UnmarshalValidateYAML([]byte(`
|
err := serialization.UnmarshalValidate([]byte(`
|
||||||
test:
|
test:
|
||||||
scheme: http
|
scheme: http
|
||||||
host: localhost
|
host: localhost
|
||||||
@@ -25,7 +26,7 @@ test:
|
|||||||
tls:
|
tls:
|
||||||
ca_file: /etc/ssl/ca.crt
|
ca_file: /etc/ssl/ca.crt
|
||||||
cert_file: /etc/ssl/cert.crt
|
cert_file: /etc/ssl/cert.crt
|
||||||
key_file: /etc/ssl/key.crt`), &cfg)
|
key_file: /etc/ssl/key.crt`), &cfg, yaml.Unmarshal)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375", TLS: &DockerTLSConfig{CAFile: "/etc/ssl/ca.crt", CertFile: "/etc/ssl/cert.crt", KeyFile: "/etc/ssl/key.crt"}}, cfg["test"])
|
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375", TLS: &DockerTLSConfig{CAFile: "/etc/ssl/ca.crt", CertFile: "/etc/ssl/cert.crt", KeyFile: "/etc/ssl/key.crt"}}, cfg["test"])
|
||||||
})
|
})
|
||||||
@@ -131,7 +132,7 @@ func TestDockerProviderConfigValidation(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
var cfg map[string]*DockerProviderConfig
|
var cfg map[string]*DockerProviderConfig
|
||||||
err := serialization.UnmarshalValidateYAML([]byte(test.yamlStr), &cfg)
|
err := serialization.UnmarshalValidate([]byte(test.yamlStr), &cfg, yaml.Unmarshal)
|
||||||
if test.wantErr {
|
if test.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user