refactor(pages): 精简部署源模型并重构详情页交互

将 Remote 网络策略收敛为 allow_insecure,去掉脱敏与无用字段;
Pages 详情拆为部署/设置 Tab,统一卡片样式与来源信息展示。
This commit is contained in:
ryan
2026-07-19 20:23:31 +08:00
parent f0b51a99b3
commit abe8fb8268
40 changed files with 1050 additions and 1314 deletions
@@ -13,8 +13,8 @@ import (
)
// downloadPagesPackageFromURL is the deprecated one-shot URL adapter. It uses
// the same bounded downloader as persisted sources, with the legacy trusted
// network policy that permits operator-managed internal artifact services.
// the same bounded downloader as persisted sources and allows insecure TLS for
// operator-managed internal artifact services.
func downloadPagesPackageFromURL(
ctx context.Context,
rawURL string,
@@ -25,7 +25,7 @@ func downloadPagesPackageFromURL(
}
candidate, err := FetchRemoteSource(ctx, RemoteSourceRequest{
URL: strings.TrimSpace(rawURL),
NetworkPolicy: RemoteNetworkPolicyTrustedInternal,
AllowInsecure: true,
MaxPackageBytes: maxPackageBytes,
})
if err != nil {
-2
View File
@@ -41,9 +41,7 @@ const (
errPagesSourceTypeUnsupported = "pages 部署源类型不受支持"
errPagesSourceRemoteFields = "远程地址来源不能包含 GitHub 或自动更新配置"
errPagesSourceRemoteURLRequired = "请提供远程部署包地址"
errPagesSourceRemoteURLMode = "remote_url_set 与 remote_url 参数不匹配"
errPagesSourceRemoteURLInvalid = "远程部署包地址无效,仅支持不含用户信息和片段的 http/https 地址"
errPagesSourceNetworkPolicy = "远程地址网络策略仅支持 public 或 trusted_internal"
errPagesSourceGitHubFields = "GitHub Release 来源不能包含远程地址配置"
errPagesSourceRepositoryInvalid = "GitHub 仓库地址无效,仅支持 https://github.com/{owner}/{repo}"
errPagesSourceSelectorInvalid = "GitHub Release 选择方式无效"
@@ -53,8 +53,7 @@ func validateGitHubSourceInput(input SourceUpdateInput) error {
if strings.TrimSpace(input.SourceType) != PagesSourceTypeGitHubRelease {
return errors.New(errPagesSourceTypeUnsupported)
}
if input.RemoteURLSet || strings.TrimSpace(input.RemoteURL) != "" ||
strings.TrimSpace(input.RemoteNetworkPolicy) != "" {
if strings.TrimSpace(input.RemoteURL) != "" || input.AllowInsecure {
return errors.New(errPagesSourceGitHubFields)
}
if _, err := normalizeGitHubRepositoryURL(input.RepositoryURL); err != nil {
@@ -275,7 +274,7 @@ func githubSourceUpdates(config githubSourceConfig, version int) map[string]any
return map[string]any{
"source_type": PagesSourceTypeGitHubRelease,
"remote_url": "",
"remote_network_policy": "",
"allow_insecure": false,
"github_repository": config.Repository,
"release_selector": config.Selector,
"release_tag": config.Tag,
@@ -289,7 +288,7 @@ func githubSourceUpdates(config githubSourceConfig, version int) map[string]any
func githubSourceConfigChanged(existing *model.PagesProjectSource, config githubSourceConfig) bool {
return existing.SourceType != PagesSourceTypeGitHubRelease || existing.RemoteURL != "" ||
existing.RemoteNetworkPolicy != "" || existing.GitHubRepository != config.Repository ||
existing.AllowInsecure || existing.GitHubRepository != config.Repository ||
existing.ReleaseSelector != config.Selector || existing.ReleaseTag != config.Tag ||
existing.AssetName != config.AssetName || existing.AutoUpdateEnabled != config.AutoUpdate ||
existing.CheckIntervalMinutes != config.CheckInterval
@@ -150,9 +150,8 @@ func TestGitHubSourceValidationNormalizationAndProviderSwitch(t *testing.T) {
secret := "provider-switch-secret"
if _, err := UpdateSource(ctx, project.ID, SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: "https://artifacts.example.com/site.zip?token=" + secret,
RemoteNetworkPolicy: RemoteNetworkPolicyPublic,
RemoteURL: "https://artifacts.example.com/site.zip?token=" + secret,
AllowInsecure: false,
}); err != nil {
t.Fatalf("UpdateSource(GitHub to Remote) error = %v, want nil", err)
}
@@ -165,8 +164,8 @@ func TestGitHubSourceValidationNormalizationAndProviderSwitch(t *testing.T) {
t.Fatalf("UpdateSourceAs(Remote to GitHub) error = %v, want nil", err)
}
github, _ := mustLoadPagesSource(t, ctx, project.ID)
if github.RemoteURL != "" || github.RemoteNetworkPolicy != "" {
t.Errorf("GitHub switched source retained Remote fields: URL=%q policy=%q", github.RemoteURL, github.RemoteNetworkPolicy)
if github.RemoteURL != "" || github.AllowInsecure {
t.Errorf("GitHub switched source retained Remote fields: URL=%q allow_insecure=%v", github.RemoteURL, github.AllowInsecure)
}
}
+1 -3
View File
@@ -42,9 +42,7 @@ func handleSourceLogicError(c *gin.Context, err error) bool {
errPagesSourceTypeUnsupported,
errPagesSourceRemoteFields,
errPagesSourceRemoteURLRequired,
errPagesSourceRemoteURLMode,
errPagesSourceRemoteURLInvalid,
errPagesSourceNetworkPolicy,
errPagesSourceGitHubFields,
errPagesSourceRepositoryInvalid,
errPagesSourceSelectorInvalid,
@@ -239,7 +237,7 @@ func DeleteProjectHandler(c *gin.Context) {
// GetSourceHandler 获取 Pages 项目的部署源。
// @Summary 获取 Pages 部署源
// @Description 返回脱敏后的项目部署源配置与运行状态,需要管理员权限
// @Description 返回项目部署源配置与运行状态,需要管理员权限
// @Tags openflare-pages
// @Produce json
// @Security SessionCookie
@@ -110,7 +110,7 @@ func TestPagesSourceHandlersReturnStableActionErrors(t *testing.T) {
ctx,
remoteProject.ID,
"https://example.com/site.zip?token=handler-secret",
RemoteNetworkPolicyPublic,
false,
)
code, envelope = performPagesSourceRequest(
t,
@@ -135,7 +135,7 @@ func TestPagesSourceHandlersReturnStableActionErrors(t *testing.T) {
ctx,
busyProject.ID,
"https://example.com/site.zip",
RemoteNetworkPolicyPublic,
false,
)
future := time.Now().Add(time.Minute)
if err := db.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
@@ -172,7 +172,7 @@ func TestSyncSourceHandlerAcceptsEmptyBodyAndEmptyObject(t *testing.T) {
ctx,
project.ID,
"https://example.com/site.zip?token=dispatch-secret",
RemoteNetworkPolicyPublic,
false,
)
path := fmt.Sprintf("/api/v1/d/pages/%d/source/sync", project.ID)
+23 -61
View File
@@ -45,12 +45,11 @@ const (
// SourceUpdateInput is the discriminated source configuration payload.
// GitHub fields are accepted by the decoder so mode-incompatible values can be
// rejected deterministically; GitHub itself is enabled in Phase 2.
// rejected deterministically.
type SourceUpdateInput struct {
SourceType string `json:"source_type"`
RemoteURLSet bool `json:"remote_url_set"`
RemoteURL string `json:"remote_url"`
RemoteNetworkPolicy string `json:"remote_network_policy"`
AllowInsecure bool `json:"allow_insecure"`
RepositoryURL string `json:"repository_url"`
ReleaseSelector string `json:"release_selector"`
ReleaseTag string `json:"release_tag"`
@@ -59,19 +58,18 @@ type SourceUpdateInput struct {
CheckIntervalMinutes int `json:"check_interval_minutes"`
}
// SourceRevisionView is a credential-free source cursor.
// SourceRevisionView is a source cursor shown to the console.
type SourceRevisionView struct {
Revision string `json:"revision"`
Label string `json:"label"`
AssetName string `json:"asset_name,omitempty"`
}
// SourceView is the safe discriminated source view returned to the console.
// SourceView is the discriminated source view returned to the console.
type SourceView struct {
SourceType string `json:"source_type"`
HasRemoteURL bool `json:"has_remote_url,omitempty"`
DisplayURL string `json:"display_url,omitempty"`
RemoteNetworkPolicy string `json:"remote_network_policy,omitempty"`
RemoteURL string `json:"remote_url,omitempty"`
AllowInsecure bool `json:"allow_insecure,omitempty"`
GitHubRepository string `json:"github_repository,omitempty"`
ReleaseSelector string `json:"release_selector,omitempty"`
ReleaseTag string `json:"release_tag,omitempty"`
@@ -115,9 +113,9 @@ type sourceDetail struct {
}
type remoteSourceConfig struct {
URL string
Policy string
Identity string
URL string
AllowInsecure bool
Identity string
}
// GetSource returns the current persisted source or a manual discriminator.
@@ -228,26 +226,22 @@ func loadProjectSourceForUpdate(tx *gorm.DB, projectID uint) (*model.PagesProjec
}
func buildRemoteSourceConfig(
existing *model.PagesProjectSource,
hasExisting bool,
_ *model.PagesProjectSource,
_ bool,
input SourceUpdateInput,
) (remoteSourceConfig, error) {
remoteURL, err := resolveUpdatedRemoteURL(existing, hasExisting, input)
if err != nil {
return remoteSourceConfig{}, err
remoteURL := strings.TrimSpace(input.RemoteURL)
if remoteURL == "" {
return remoteSourceConfig{}, errors.New(errPagesSourceRemoteURLRequired)
}
parsedURL, err := parseRemoteSourceURL(remoteURL)
if err != nil {
return remoteSourceConfig{}, err
}
policy := strings.TrimSpace(input.RemoteNetworkPolicy)
if policy == "" {
policy = RemoteNetworkPolicyPublic
}
return remoteSourceConfig{
URL: remoteURL,
Policy: policy,
Identity: remoteSourceIdentity(parsedURL),
URL: remoteURL,
AllowInsecure: input.AllowInsecure,
Identity: remoteSourceIdentity(parsedURL),
}, nil
}
@@ -256,7 +250,7 @@ func createRemoteSourceTx(tx *gorm.DB, projectID uint, config remoteSourceConfig
ProjectID: projectID,
SourceType: PagesSourceTypeRemoteURL,
RemoteURL: config.URL,
RemoteNetworkPolicy: config.Policy,
AllowInsecure: config.AllowInsecure,
AutoUpdateEnabled: false,
CheckIntervalMinutes: 0,
ConfigVersion: 1,
@@ -289,7 +283,7 @@ func updateExistingRemoteSourceTx(
if err := tx.Model(existing).Updates(map[string]any{
"source_type": PagesSourceTypeRemoteURL,
"remote_url": config.URL,
"remote_network_policy": config.Policy,
"allow_insecure": config.AllowInsecure,
"github_repository": "",
"release_selector": "",
"release_tag": "",
@@ -307,7 +301,7 @@ func updateExistingRemoteSourceTx(
func remoteSourceConfigChanged(existing *model.PagesProjectSource, config remoteSourceConfig) bool {
return existing.SourceType != PagesSourceTypeRemoteURL ||
existing.RemoteURL != config.URL ||
existing.RemoteNetworkPolicy != config.Policy ||
existing.AllowInsecure != config.AllowInsecure ||
existing.GitHubRepository != "" ||
existing.ReleaseSelector != "" ||
existing.ReleaseTag != "" ||
@@ -363,14 +357,7 @@ func validateRemoteSourceInput(input SourceUpdateInput) error {
input.AutoUpdateEnabled || input.CheckIntervalMinutes != 0 {
return errors.New(errPagesSourceRemoteFields)
}
policy := strings.TrimSpace(input.RemoteNetworkPolicy)
if policy != "" && policy != RemoteNetworkPolicyPublic && policy != RemoteNetworkPolicyTrustedInternal {
return errors.New(errPagesSourceNetworkPolicy)
}
if !input.RemoteURLSet && strings.TrimSpace(input.RemoteURL) != "" {
return errors.New(errPagesSourceRemoteURLMode)
}
if input.RemoteURLSet && strings.TrimSpace(input.RemoteURL) == "" {
if strings.TrimSpace(input.RemoteURL) == "" {
return errors.New(errPagesSourceRemoteURLRequired)
}
return nil
@@ -389,16 +376,6 @@ func validateSourceUpdateInput(input SourceUpdateInput) error {
}
}
func resolveUpdatedRemoteURL(existing *model.PagesProjectSource, hasExisting bool, input SourceUpdateInput) (string, error) {
if input.RemoteURLSet {
return strings.TrimSpace(input.RemoteURL), nil
}
if !hasExisting || existing.SourceType != PagesSourceTypeRemoteURL || strings.TrimSpace(existing.RemoteURL) == "" {
return "", errors.New(errPagesSourceRemoteURLRequired)
}
return existing.RemoteURL, nil
}
func parseRemoteSourceURL(raw string) (*url.URL, error) {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || parsed.Host == "" || parsed.User != nil || parsed.Fragment != "" {
@@ -436,20 +413,6 @@ func remoteSourceIdentity(parsed *url.URL) string {
return hex.EncodeToString(sum[:])
}
func displayRemoteSourceURL(raw string) string {
parsed, err := parseRemoteSourceURL(raw)
if err != nil {
return ""
}
hadQuery := parsed.RawQuery != ""
parsed.RawQuery = ""
display := parsed.String()
if hadQuery {
display += "?***"
}
return display
}
func loadSourceByProject(ctx context.Context, projectID uint) (*model.PagesProjectSource, *model.PagesProjectSourceRuntime, error) {
var source model.PagesProjectSource
if err := db.DB(ctx).Where("project_id = ?", projectID).First(&source).Error; err != nil {
@@ -477,9 +440,8 @@ func buildSourceView(source *model.PagesProjectSource, runtime *model.PagesProje
}
switch source.SourceType {
case PagesSourceTypeRemoteURL:
view.HasRemoteURL = strings.TrimSpace(source.RemoteURL) != ""
view.DisplayURL = displayRemoteSourceURL(source.RemoteURL)
view.RemoteNetworkPolicy = source.RemoteNetworkPolicy
view.RemoteURL = source.RemoteURL
view.AllowInsecure = source.AllowInsecure
case PagesSourceTypeGitHubRelease:
view.LastCheckedAt = runtime.LastCheckedAt
view.NextCheckAt = runtime.NextCheckAt
+14 -159
View File
@@ -26,13 +26,6 @@ import (
)
const (
// RemoteNetworkPolicyPublic only permits publicly routable targets and
// performs DNS validation again for every connection.
RemoteNetworkPolicyPublic = "public"
// RemoteNetworkPolicyTrustedInternal permits private targets and self-signed
// TLS certificates. It is an explicit administrator trust boundary.
RemoteNetworkPolicyTrustedInternal = "trusted_internal"
remoteSourceDownloadTimeout = 10 * time.Minute
remoteSourceResponseHeaderTimeout = 30 * time.Second
remoteSourceDialTimeout = 30 * time.Second
@@ -52,12 +45,9 @@ func (providerError remoteProviderError) Error() string {
return string(providerError)
}
const (
errRemoteProviderInvalidPolicy remoteProviderError = "远程来源网络策略无效"
errRemoteProviderInvalidLimit remoteProviderError = "远程来源部署包大小限制无效"
errRemoteProviderBlockedAddress remoteProviderError = "远程来源 public 策略禁止访问非公网地址"
errRemoteProviderResolveFailed remoteProviderError = "远程来源地址解析失败"
errRemoteProviderRedirectLimit remoteProviderError = "远程来源重定向次数超过限制"
const (
errRemoteProviderInvalidLimit remoteProviderError = "远程来源部署包大小限制无效"
errRemoteProviderRedirectLimit remoteProviderError = "远程来源重定向次数超过限制"
errRemoteProviderDownloadFailed remoteProviderError = errPagesPackageURLDownloadFailed
errRemoteProviderTooLarge remoteProviderError = errPagesPackageURLTooLarge
errRemoteProviderEmpty remoteProviderError = errPagesPackageEmpty
@@ -65,38 +55,10 @@ const (
errRemoteProviderCleanupFailed remoteProviderError = "清理远程来源临时文件失败"
)
var remoteSourceNonPublicPrefixes = []netip.Prefix{
// IPv4 special-use, private, link-local, documentation, multicast and
// reserved ranges. A conservative deny list is intentional for SSRF safety.
netip.MustParsePrefix("0.0.0.0/8"),
netip.MustParsePrefix("10.0.0.0/8"),
netip.MustParsePrefix("100.64.0.0/10"),
netip.MustParsePrefix("127.0.0.0/8"),
netip.MustParsePrefix("169.254.0.0/16"),
netip.MustParsePrefix("172.16.0.0/12"),
netip.MustParsePrefix("192.0.0.0/24"),
netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.88.99.0/24"),
netip.MustParsePrefix("192.168.0.0/16"),
netip.MustParsePrefix("198.18.0.0/15"),
netip.MustParsePrefix("198.51.100.0/24"),
netip.MustParsePrefix("203.0.113.0/24"),
netip.MustParsePrefix("224.0.0.0/4"),
netip.MustParsePrefix("240.0.0.0/4"),
// IPv6 protocol-assignment, documentation and transition ranges that are
// not acceptable as direct public artifact origins.
netip.MustParsePrefix("2001::/23"),
netip.MustParsePrefix("2001:db8::/32"),
netip.MustParsePrefix("2002::/16"),
netip.MustParsePrefix("3fff::/20"),
}
var remoteSourcePublicIPv6Prefix = netip.MustParsePrefix("2000::/3")
// RemoteSourceRequest describes one immutable Remote URL package fetch.
type RemoteSourceRequest struct {
URL string
NetworkPolicy string
AllowInsecure bool
MaxPackageBytes int64
}
@@ -158,20 +120,16 @@ func fetchRemoteSource(ctx context.Context, request RemoteSourceRequest, depende
if dependencies.dialContext == nil || dependencies.createTemp == nil {
return nil, errRemoteProviderDownloadFailed
}
policy, err := normalizeRemoteNetworkPolicy(request.NetworkPolicy)
if err != nil {
return nil, err
}
parsed, err := parseRemoteSourceURL(request.URL)
if err != nil {
return nil, err
}
if err := validateRemoteSourceTarget(ctx, parsed, policy, dependencies.resolver); err != nil {
if err := validateRemoteSourceTarget(ctx, parsed); err != nil {
return nil, sanitizeRemoteProviderError(ctx, err)
}
safeLabel, namedFormat := remoteSourceLabel(parsed)
client := newRemoteSourceClient(policy, dependencies)
client := newRemoteSourceClient(request.AllowInsecure, dependencies)
defer client.CloseIdleConnections()
response, err := requestRemoteSource(ctx, client, parsed)
if err != nil {
@@ -211,33 +169,18 @@ func fetchRemoteSource(ctx context.Context, request RemoteSourceRequest, depende
}, nil
}
func normalizeRemoteNetworkPolicy(policy string) (string, error) {
switch strings.TrimSpace(policy) {
case "", RemoteNetworkPolicyPublic:
return RemoteNetworkPolicyPublic, nil
case RemoteNetworkPolicyTrustedInternal:
return RemoteNetworkPolicyTrustedInternal, nil
default:
return "", errRemoteProviderInvalidPolicy
}
}
func newRemoteSourceClient(policy string, dependencies remoteSourceDependencies) *http.Client {
func newRemoteSourceClient(allowInsecure bool, dependencies remoteSourceDependencies) *http.Client {
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
dialContext := dependencies.dialContext
if policy == RemoteNetworkPolicyPublic {
dialContext = newPublicRemoteSourceDialer(dependencies.resolver, dependencies.dialContext)
} else {
// trusted_internal is an explicit administrator-selected boundary for
// private artifact services using an internal CA or self-signed cert.
tlsConfig.InsecureSkipVerify = true //nolint:gosec // required trusted_internal semantics
if allowInsecure {
// Explicit administrator choice for self-signed or private CA endpoints.
tlsConfig.InsecureSkipVerify = true //nolint:gosec // required allow_insecure semantics
}
client := &http.Client{
Timeout: remoteSourceDownloadTimeout,
Transport: httppool.NewTransport(httppool.TransportOptions{
Proxy: nil,
DialContext: dialContext,
DialContext: dependencies.dialContext,
TLSClientConfig: tlsConfig,
ResponseHeaderTimeout: remoteSourceResponseHeaderTimeout,
TraceFilter: remoteSourceTraceFilter,
@@ -248,7 +191,7 @@ func newRemoteSourceClient(policy string, dependencies remoteSourceDependencies)
return errRemoteProviderRedirectLimit
}
stripRemoteSourceRedirectHeaders(next)
if err := validateRemoteSourceTarget(next.Context(), next.URL, policy, dependencies.resolver); err != nil {
if err := validateRemoteSourceTarget(next.Context(), next.URL); err != nil {
return err
}
applyRemoteSourceHeaders(next)
@@ -293,7 +236,7 @@ func remoteSourceTraceFilter(request *http.Request) bool {
return request.URL == nil || request.URL.RawQuery == ""
}
func validateRemoteSourceTarget(ctx context.Context, target *url.URL, policy string, resolver remoteSourceResolver) error {
func validateRemoteSourceTarget(_ context.Context, target *url.URL) error {
if target == nil || target.User != nil || target.Fragment != "" || target.Opaque != "" {
return errors.New(errPagesSourceRemoteURLInvalid)
}
@@ -301,92 +244,7 @@ func validateRemoteSourceTarget(ctx context.Context, target *url.URL, policy str
if (scheme != remoteSourceSchemeHTTP && scheme != remoteSourceSchemeHTTPS) || strings.TrimSpace(target.Hostname()) == "" {
return errors.New(errPagesSourceRemoteURLInvalid)
}
if policy != RemoteNetworkPolicyPublic {
return nil
}
_, err := resolvePublicRemoteSourceIPs(ctx, resolver, target.Hostname())
return err
}
func newPublicRemoteSourceDialer(
resolver remoteSourceResolver,
directDial func(context.Context, string, string) (net.Conn, error),
) func(context.Context, string, string) (net.Conn, error) {
return func(ctx context.Context, network string, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, errRemoteProviderDownloadFailed
}
addresses, err := resolvePublicRemoteSourceIPs(ctx, resolver, host)
if err != nil {
return nil, err
}
for _, address := range addresses {
if !remoteSourceIPMatchesNetwork(address, network) {
continue
}
connection, dialErr := directDial(ctx, network, net.JoinHostPort(address.String(), port))
if dialErr == nil {
return connection, nil
}
}
return nil, errRemoteProviderDownloadFailed
}
}
func resolvePublicRemoteSourceIPs(ctx context.Context, resolver remoteSourceResolver, host string) ([]netip.Addr, error) {
if strings.Contains(host, "%") {
return nil, errRemoteProviderBlockedAddress
}
if literal, parseErr := netip.ParseAddr(host); parseErr == nil {
if !isPublicRemoteSourceIP(literal) {
return nil, errRemoteProviderBlockedAddress
}
return []netip.Addr{literal}, nil
}
if resolver == nil {
return nil, errRemoteProviderResolveFailed
}
addresses, err := resolver.LookupNetIP(ctx, "ip", host)
if err != nil || len(addresses) == 0 {
return nil, errRemoteProviderResolveFailed
}
for _, address := range addresses {
if !isPublicRemoteSourceIP(address) {
return nil, errRemoteProviderBlockedAddress
}
}
return addresses, nil
}
func isPublicRemoteSourceIP(address netip.Addr) bool {
if !address.IsValid() || address.Zone() != "" {
return false
}
address = address.Unmap()
if !address.IsGlobalUnicast() {
return false
}
if address.Is6() && !remoteSourcePublicIPv6Prefix.Contains(address) {
return false
}
for _, prefix := range remoteSourceNonPublicPrefixes {
if prefix.Contains(address) {
return false
}
}
return true
}
func remoteSourceIPMatchesNetwork(address netip.Addr, network string) bool {
switch network {
case "tcp4":
return address.Unmap().Is4()
case "tcp6":
return address.Unmap().Is6()
default:
return true
}
return nil
}
func streamRemoteSourcePackage(
@@ -531,10 +389,7 @@ func sanitizeRemoteProviderError(ctx context.Context, err error) error {
return fmt.Errorf("%w: %w", errRemoteProviderDownloadFailed, ctxErr)
}
for _, safeError := range []error{
errRemoteProviderInvalidPolicy,
errRemoteProviderInvalidLimit,
errRemoteProviderBlockedAddress,
errRemoteProviderResolveFailed,
errRemoteProviderRedirectLimit,
errRemoteProviderTooLarge,
errRemoteProviderEmpty,
@@ -50,7 +50,7 @@ func TestFetchRemoteSourceTrustedInternalSelfSignedAndSafeLabel(t *testing.T) {
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: server.URL + "/original/site.zip?token=source-secret",
NetworkPolicy: RemoteNetworkPolicyTrustedInternal,
AllowInsecure: true,
MaxPackageBytes: int64(len(packageBytes) + 1),
})
if err != nil {
@@ -106,7 +106,7 @@ func TestFetchRemoteSourceKeepsOriginalLabelAcrossRedirect(t *testing.T) {
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: server.URL + "/original/site.zip?token=initial-secret",
NetworkPolicy: RemoteNetworkPolicyTrustedInternal,
AllowInsecure: true,
MaxPackageBytes: int64(len(packageBytes) + 1),
})
if err != nil {
@@ -118,29 +118,28 @@ func TestFetchRemoteSourceKeepsOriginalLabelAcrossRedirect(t *testing.T) {
}
}
func TestFetchRemoteSourcePublicRejectsNonPublicAddresses(t *testing.T) {
tests := []string{
"http://127.0.0.1/site.zip?token=loopback-secret",
"http://[::1]/site.zip?token=ipv6-secret",
"http://100.64.0.1/site.zip?token=cgnat-secret",
"http://192.0.2.1/site.zip?token=documentation-secret",
func TestFetchRemoteSourcePublicAllowsPrivateAddresses(t *testing.T) {
packageBytes := makeRemoteSourceZIP(t)
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
_, _ = writer.Write(packageBytes)
}))
t.Cleanup(server.Close)
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: server.URL + "/site.zip?token=private-secret",
AllowInsecure: false,
MaxPackageBytes: int64(len(packageBytes) + 1),
})
if err != nil {
t.Fatalf("FetchRemoteSource() error = %v", err)
}
for _, rawURL := range tests {
t.Run(rawURL, func(t *testing.T) {
_, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: rawURL,
NetworkPolicy: RemoteNetworkPolicyPublic,
MaxPackageBytes: 1024,
})
if !errors.Is(err, errRemoteProviderBlockedAddress) {
t.Fatalf("FetchRemoteSource() error = %v, want blocked address", err)
}
assertRemoteSourceErrorRedacted(t, err, rawURL, "secret", "token=")
})
defer func() { _ = candidate.Cleanup() }()
if candidate.Format != "zip" {
t.Fatalf("candidate format = %q, want zip", candidate.Format)
}
}
func TestFetchRemoteSourcePublicDialsValidatedIP(t *testing.T) {
func TestFetchRemoteSourcePublicUsesDirectDialer(t *testing.T) {
packageBytes := makeRemoteSourceZIP(t)
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
_, _ = writer.Write(packageBytes)
@@ -156,76 +155,44 @@ func TestFetchRemoteSourcePublicDialsValidatedIP(t *testing.T) {
}
candidate, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: "http://artifact.example/site.zip",
NetworkPolicy: RemoteNetworkPolicyPublic,
AllowInsecure: false,
MaxPackageBytes: int64(len(packageBytes) + 1),
}, dependencies)
if err != nil {
t.Fatalf("fetchRemoteSource() error = %v", err)
}
defer func() { _ = candidate.Cleanup() }()
if dialedAddress != "93.184.216.34:80" {
t.Fatalf("direct dial address = %q, want validated IP", dialedAddress)
if dialedAddress != "artifact.example:80" {
t.Fatalf("direct dial address = %q, want hostname dial", dialedAddress)
}
}
func TestFetchRemoteSourcePublicRejectsPrivateRedirect(t *testing.T) {
var requestCount atomic.Int32
func TestFetchRemoteSourcePublicAllowsPrivateRedirect(t *testing.T) {
packageBytes := makeRemoteSourceZIP(t)
var privateServer *httptest.Server
privateServer = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
_, _ = writer.Write(packageBytes)
}))
t.Cleanup(privateServer.Close)
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
requestCount.Add(1)
writer.Header().Set("Location", "http://127.0.0.1/private.zip?token=redirect-secret")
writer.Header().Set("Location", privateServer.URL+"/private.zip?token=redirect-secret")
writer.WriteHeader(http.StatusFound)
}))
t.Cleanup(server.Close)
dependencies := mappedRemoteSourceDependencies(server.Listener.Addr().String(), staticPublicRemoteSourceResolver())
rawURL := "http://artifact.example/start.zip?token=initial-secret"
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: rawURL,
NetworkPolicy: RemoteNetworkPolicyPublic,
MaxPackageBytes: 1024,
}, dependencies)
if !errors.Is(err, errRemoteProviderBlockedAddress) {
t.Fatalf("fetchRemoteSource() error = %v, want blocked redirect", err)
}
if requestCount.Load() != 1 {
t.Fatalf("request count = %d, private redirect must not be requested", requestCount.Load())
}
assertRemoteSourceErrorRedacted(t, err, rawURL, "initial-secret", "redirect-secret", "token=")
}
func TestFetchRemoteSourcePublicRejectsDNSRebinding(t *testing.T) {
var lookupCount atomic.Int32
resolver := remoteSourceResolverFunc(func(context.Context, string, string) ([]netip.Addr, error) {
if lookupCount.Add(1) == 1 {
return []netip.Addr{netip.MustParseAddr("93.184.216.34")}, nil
}
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: server.URL + "/start.zip?token=initial-secret",
AllowInsecure: false,
MaxPackageBytes: int64(len(packageBytes) + 1),
})
var dialCount atomic.Int32
dependencies := remoteSourceDependencies{
resolver: resolver,
dialContext: func(context.Context, string, string) (net.Conn, error) {
dialCount.Add(1)
return nil, errors.New("unexpected dial")
},
createTemp: os.CreateTemp,
if err != nil {
t.Fatalf("FetchRemoteSource() error = %v", err)
}
rawURL := "http://rebind.example/site.zip?signature=dns-secret"
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: rawURL,
NetworkPolicy: RemoteNetworkPolicyPublic,
MaxPackageBytes: 1024,
}, dependencies)
if !errors.Is(err, errRemoteProviderBlockedAddress) {
t.Fatalf("fetchRemoteSource() error = %v, want DNS rebinding rejection", err)
defer func() { _ = candidate.Cleanup() }()
if candidate.Format != "zip" {
t.Fatalf("candidate format = %q, want zip", candidate.Format)
}
if lookupCount.Load() != 2 {
t.Fatalf("DNS lookup count = %d, want preflight plus dial validation", lookupCount.Load())
}
if dialCount.Load() != 0 {
t.Fatalf("direct dial count = %d, rebound address must not be dialed", dialCount.Load())
}
assertRemoteSourceErrorRedacted(t, err, rawURL, "dns-secret", "signature=")
}
func TestFetchRemoteSourcePublicRejectsSelfSignedTLS(t *testing.T) {
@@ -240,7 +207,7 @@ func TestFetchRemoteSourcePublicRejectsSelfSignedTLS(t *testing.T) {
rawURL := "https://artifact.example/site.zip?signature=tls-secret"
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: rawURL,
NetworkPolicy: RemoteNetworkPolicyPublic,
AllowInsecure: false,
MaxPackageBytes: int64(len(packageBytes) + 1),
}, dependencies)
if !errors.Is(err, errRemoteProviderDownloadFailed) {
@@ -268,7 +235,7 @@ func TestFetchRemoteSourceRejectsChunkedBodyOverLimitAndCleansTemp(t *testing.T)
rawURL := server.URL + "/site.zip?token=chunk-secret"
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: rawURL,
NetworkPolicy: RemoteNetworkPolicyTrustedInternal,
AllowInsecure: true,
MaxPackageBytes: maxPackageBytes,
}, dependencies)
if !errors.Is(err, errRemoteProviderTooLarge) {
@@ -293,7 +260,7 @@ func TestFetchRemoteSourceRejectsContentLengthBeforeCreatingTemp(t *testing.T) {
}
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: server.URL + "/site.zip",
NetworkPolicy: RemoteNetworkPolicyTrustedInternal,
AllowInsecure: true,
MaxPackageBytes: 1024,
}, dependencies)
if !errors.Is(err, errRemoteProviderTooLarge) {
@@ -314,7 +281,7 @@ func TestFetchRemoteSourceSniffsAtLeast512BytesForTar(t *testing.T) {
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: server.URL + "/download",
NetworkPolicy: RemoteNetworkPolicyTrustedInternal,
AllowInsecure: true,
MaxPackageBytes: int64(len(packageBytes) + 1),
})
if err != nil {
@@ -344,7 +311,7 @@ func TestFetchRemoteSourceRedactsURLHeadersAndBodyFromErrors(t *testing.T) {
rawURL := server.URL + "/download?token=query-secret"
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: rawURL,
NetworkPolicy: RemoteNetworkPolicyTrustedInternal,
AllowInsecure: true,
MaxPackageBytes: 1024,
}, dependencies)
if !errors.Is(err, errRemoteProviderUnsupported) {
@@ -380,7 +347,7 @@ func TestFetchRemoteSourceAllowsFiveRedirectsOnly(t *testing.T) {
dependencies := mappedRemoteSourceDependencies(server.Listener.Addr().String(), staticPublicRemoteSourceResolver())
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
URL: "http://artifact.example/0",
NetworkPolicy: RemoteNetworkPolicyPublic,
AllowInsecure: false,
MaxPackageBytes: int64(len(packageBytes) + 1),
}, dependencies)
if !errors.Is(err, errRemoteProviderRedirectLimit) {
@@ -54,7 +54,7 @@ type sourceExecutionSnapshot struct {
SourceType string
SourceIdentity string
RemoteURL string
RemoteNetworkPolicy string
AllowInsecure bool
GitHubRepository string
ReleaseSelector string
ReleaseTag string
@@ -176,7 +176,7 @@ func loadSourceExecutionSnapshot(
SourceType: source.SourceType,
SourceIdentity: source.SourceIdentity,
RemoteURL: source.RemoteURL,
RemoteNetworkPolicy: source.RemoteNetworkPolicy,
AllowInsecure: source.AllowInsecure,
GitHubRepository: source.GitHubRepository,
ReleaseSelector: source.ReleaseSelector,
ReleaseTag: source.ReleaseTag,
@@ -22,7 +22,7 @@ func TestSourceLeaseHeartbeatRenewsAndCancelsOnOwnershipLoss(t *testing.T) {
ctx,
project.ID,
"https://example.com/site.zip",
RemoteNetworkPolicyPublic,
false,
)
snapshot, outcome, err := acquireSourceLease(ctx, source.ID, source.ConfigVersion, sourceActionSync)
if err != nil || outcome != sourceLeaseAcquired || snapshot == nil {
@@ -83,7 +83,7 @@ func TestAcquireSourceLeaseConcurrentOnlyOneOwner(t *testing.T) {
ctx,
project.ID,
"https://example.com/site.zip",
RemoteNetworkPolicyPublic,
false,
)
type leaseResult struct {
snapshot *sourceExecutionSnapshot
@@ -138,7 +138,7 @@ func TestAcquireSourceLeaseMutualExclusionExpiryAndTerminalOwnership(t *testing.
ctx,
project.ID,
"https://example.com/site.zip",
RemoteNetworkPolicyPublic,
false,
)
first, outcome, err := acquireSourceLease(ctx, source.ID, source.ConfigVersion, sourceActionSync)
@@ -234,7 +234,7 @@ func TestSourceConfigAndProjectContentChangesFenceLease(t *testing.T) {
ctx,
project.ID,
"https://example.com/site.zip?token=first",
RemoteNetworkPolicyPublic,
false,
)
configSnapshot, outcome, err := acquireSourceLease(ctx, source.ID, source.ConfigVersion, sourceActionSync)
@@ -243,9 +243,8 @@ func TestSourceConfigAndProjectContentChangesFenceLease(t *testing.T) {
}
if _, err := UpdateSource(ctx, project.ID, SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: "https://example.com/site.zip?token=second",
RemoteNetworkPolicy: RemoteNetworkPolicyPublic,
AllowInsecure: false,
}); err != nil {
t.Fatalf("UpdateSource(config fence) error = %v, want nil", err)
}
+1 -1
View File
@@ -268,7 +268,7 @@ func prepareRemoteSource(
task.AppendLog(ctx, "[download] 正在获取远程部署包")
candidate, err := FetchRemoteSource(ctx, RemoteSourceRequest{
URL: snapshot.RemoteURL,
NetworkPolicy: snapshot.RemoteNetworkPolicy,
AllowInsecure: snapshot.AllowInsecure,
MaxPackageBytes: limits.PackageBytes,
})
if err != nil {
@@ -109,7 +109,7 @@ func TestSyncRemoteSourceAtomicallyActivatesAndReusesChecksum(t *testing.T) {
ctx,
project.ID,
server.URL+"/site.zip?token="+secret,
RemoteNetworkPolicyTrustedInternal,
true,
)
firstSnapshot := mustAcquireRemoteSyncLease(t, ctx, source)
@@ -220,7 +220,7 @@ func TestSyncRemoteSourceDownloadFailureKeepsOldActive(t *testing.T) {
ctx,
project.ID,
server.URL+"/site.zip?token="+secret,
RemoteNetworkPolicyTrustedInternal,
true,
)
_, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:2")
@@ -244,7 +244,7 @@ func TestSyncRemoteSourceArchiveFailureKeepsOldActive(t *testing.T) {
ctx,
project.ID,
server.URL+"/site.zip?token="+secret,
RemoteNetworkPolicyTrustedInternal,
true,
)
_, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:3")
@@ -275,7 +275,7 @@ func TestSyncRemoteSourceFinalFenceCompensatesIngest(t *testing.T) {
ctx,
project.ID,
server.URL+"/site.zip?token=final-fence-secret",
RemoteNetworkPolicyTrustedInternal,
true,
)
outcome, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:4")
@@ -345,7 +345,7 @@ func TestCommitSourceDeploymentRechecksLeaseAfterUploadLocks(t *testing.T) {
ctx,
project.ID,
server.URL+"/site.zip",
RemoteNetworkPolicyTrustedInternal,
true,
)
first, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:5")
if err != nil || first == nil || first.Deployment == nil {
@@ -427,7 +427,7 @@ func TestCompensateSourceIngestSurvivesCanceledParentContext(t *testing.T) {
ctx,
project.ID,
"https://example.com/site.zip",
RemoteNetworkPolicyPublic,
false,
)
packageBytes := testPagesZip(t, map[string]string{"index.html": "cancel-compensation"})
packagePath := filepath.Join(t.TempDir(), "site.zip")
@@ -517,7 +517,7 @@ func TestCommitSourceDeploymentRejectsDeletedTargetUpload(t *testing.T) {
ctx,
project.ID,
server.URL+"/site.zip",
RemoteNetworkPolicyTrustedInternal,
true,
)
snapshot := mustAcquireRemoteSyncLease(t, ctx, source)
@@ -278,7 +278,6 @@ func isPermanentSourceSyncError(err error) bool {
strings.Contains(message, errPagesPackageFileTooLarge) ||
strings.Contains(message, errPagesEntryFileMissing) ||
strings.Contains(message, errPagesSourceRemoteURLInvalid) ||
strings.Contains(message, errPagesSourceNetworkPolicy) ||
strings.Contains(message, errPagesSourceReleaseNotFound) ||
strings.Contains(message, errPagesSourceDigestInvalid) ||
strings.Contains(message, errPagesSourceDigestMismatch) ||
@@ -135,7 +135,7 @@ func TestRemoteCheckActionIsPermanentWithoutExposingURL(t *testing.T) {
ctx,
project.ID,
"https://example.com/site.zip?token="+secret,
RemoteNetworkPolicyPublic,
false,
)
raw, err := json.Marshal(SourceActionPayload{
SourceID: source.ID,
+26 -65
View File
@@ -5,7 +5,6 @@ package pages
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
@@ -80,14 +79,13 @@ func mustConfigureRemoteSource(
ctx context.Context,
projectID uint,
remoteURL string,
policy string,
allowInsecure bool,
) (*model.PagesProjectSource, *model.PagesProjectSourceRuntime) {
t.Helper()
_, err := UpdateSource(ctx, projectID, SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: remoteURL,
RemoteNetworkPolicy: policy,
SourceType: PagesSourceTypeRemoteURL,
RemoteURL: remoteURL,
AllowInsecure: allowInsecure,
})
if err != nil {
t.Fatalf("UpdateSource(%d, %q) error = %v, want nil", projectID, remoteURL, err)
@@ -111,23 +109,14 @@ func TestValidateRemoteSourceInputRejectsModeIncompatibleFields(t *testing.T) {
{
name: "missing source type",
input: SourceUpdateInput{
RemoteURLSet: true,
RemoteURL: "https://example.com/site.zip",
},
},
{
name: "github type reserved for phase two",
input: SourceUpdateInput{
SourceType: PagesSourceTypeGitHubRelease,
RepositoryURL: "https://github.com/example/site",
RemoteURL: "https://example.com/site.zip",
},
},
{
name: "remote rejects repository field",
input: SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: "https://example.com/site.zip",
RemoteURL: "https://example.com/site.zip",
RepositoryURL: "https://github.com/example/site",
},
},
@@ -135,25 +124,14 @@ func TestValidateRemoteSourceInputRejectsModeIncompatibleFields(t *testing.T) {
name: "remote rejects automatic updates",
input: SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: "https://example.com/site.zip",
RemoteURL: "https://example.com/site.zip",
AutoUpdateEnabled: true,
},
},
{
name: "url value requires replacement flag",
name: "missing remote url",
input: SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURL: "https://example.com/site.zip",
},
},
{
name: "invalid network policy",
input: SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: "https://example.com/site.zip",
RemoteNetworkPolicy: "private",
},
},
}
@@ -185,7 +163,7 @@ func TestRemoteSourceCRUDPreservesSecretAndResetsRuntimeByIdentity(t *testing.T)
ctx := setupPagesSourceTest(t)
project := mustCreatePagesSourceProject(t, ctx, "remote-crud")
firstURL := "https://Artifacts.Example.com:443/dist/site.zip?token=first-secret&expires=1"
source, runtime := mustConfigureRemoteSource(t, ctx, project.ID, firstURL, RemoteNetworkPolicyPublic)
source, runtime := mustConfigureRemoteSource(t, ctx, project.ID, firstURL, false)
if got, want := source.ConfigVersion, 1; got != want {
t.Errorf("new source ConfigVersion = %d, want %d", got, want)
@@ -197,21 +175,12 @@ func TestRemoteSourceCRUDPreservesSecretAndResetsRuntimeByIdentity(t *testing.T)
if err != nil {
t.Fatalf("GetSource(%d) error = %v, want nil", project.ID, err)
}
if got, want := view.DisplayURL, "https://Artifacts.Example.com:443/dist/site.zip?***"; got != want {
t.Errorf("GetSource(%d).DisplayURL = %q, want %q", project.ID, got, want)
}
encodedView, err := json.Marshal(view)
if err != nil {
t.Fatalf("json.Marshal(GetSource(%d)) error = %v, want nil", project.ID, err)
}
if strings.Contains(string(encodedView), "first-secret") || strings.Contains(string(encodedView), "expires=1") {
t.Errorf("GetSource(%d) JSON = %s, want credential-free view", project.ID, encodedView)
if got, want := view.RemoteURL, firstURL; got != want {
t.Errorf("GetSource(%d).RemoteURL = %q, want %q", project.ID, got, want)
}
if _, err := UpdateSource(ctx, project.ID, SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: firstURL,
RemoteNetworkPolicy: RemoteNetworkPolicyPublic,
SourceType: PagesSourceTypeRemoteURL,
RemoteURL: firstURL,
}); err != nil {
t.Fatalf("UpdateSource(%d, no-op) error = %v, want nil", project.ID, err)
}
@@ -240,12 +209,12 @@ func TestRemoteSourceCRUDPreservesSecretAndResetsRuntimeByIdentity(t *testing.T)
t.Fatalf("seed source runtime error = %v, want nil", err)
}
// Omit the secret URL while changing policy. The stored URL and cursor must
// Keep the same URL while enabling insecure TLS. The identity and cursor must
// survive, while the in-flight lease is fenced.
if _, err := UpdateSource(ctx, project.ID, SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: false,
RemoteNetworkPolicy: RemoteNetworkPolicyTrustedInternal,
SourceType: PagesSourceTypeRemoteURL,
RemoteURL: firstURL,
AllowInsecure: true,
}); err != nil {
t.Fatalf("UpdateSource(%d, preserve URL) error = %v, want nil", project.ID, err)
}
@@ -275,10 +244,9 @@ func TestRemoteSourceCRUDPreservesSecretAndResetsRuntimeByIdentity(t *testing.T)
// Replacing only the query secret keeps the canonical identity and cursors.
queryReplacementURL := "https://artifacts.example.com/dist/site.zip?token=second-secret"
if _, err := UpdateSource(ctx, project.ID, SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: queryReplacementURL,
RemoteNetworkPolicy: RemoteNetworkPolicyTrustedInternal,
SourceType: PagesSourceTypeRemoteURL,
RemoteURL: queryReplacementURL,
AllowInsecure: true,
}); err != nil {
t.Fatalf("UpdateSource(%d, query replacement) error = %v, want nil", project.ID, err)
}
@@ -296,10 +264,9 @@ func TestRemoteSourceCRUDPreservesSecretAndResetsRuntimeByIdentity(t *testing.T)
// Replacing the path changes identity and clears all remote cursors.
pathReplacementURL := "https://artifacts.example.com/dist/other.zip?token=third-secret"
if _, err := UpdateSource(ctx, project.ID, SourceUpdateInput{
SourceType: PagesSourceTypeRemoteURL,
RemoteURLSet: true,
RemoteURL: pathReplacementURL,
RemoteNetworkPolicy: RemoteNetworkPolicyTrustedInternal,
SourceType: PagesSourceTypeRemoteURL,
RemoteURL: pathReplacementURL,
AllowInsecure: true,
}); err != nil {
t.Fatalf("UpdateSource(%d, path replacement) error = %v, want nil", project.ID, err)
}
@@ -320,14 +287,8 @@ func TestRemoteSourceCRUDPreservesSecretAndResetsRuntimeByIdentity(t *testing.T)
if err != nil {
t.Fatalf("GetSource(%d) after path replacement error = %v, want nil", project.ID, err)
}
pathJSON, err := json.Marshal(pathView)
if err != nil {
t.Fatalf("json.Marshal(path view) error = %v, want nil", err)
}
for _, secret := range []string{"first-secret", "second-secret", "third-secret"} {
if strings.Contains(string(pathJSON), secret) {
t.Errorf("path view JSON = %s, want no secret %q", pathJSON, secret)
}
if got, want := pathView.RemoteURL, pathReplacementURL; got != want {
t.Errorf("GetSource(%d).RemoteURL = %q, want %q", project.ID, got, want)
}
}
@@ -353,7 +314,7 @@ func TestDeleteSourceIsIdempotentAndKeepsDeploymentState(t *testing.T) {
ctx,
project.ID,
"https://example.com/site.zip?token=delete-secret",
RemoteNetworkPolicyPublic,
false,
)
deployment := &model.PagesDeployment{
ProjectID: project.ID,