Skip to content

Commit f932a0c

Browse files
author
gitlayzer
committed
fix: close consistency and rollback gaps
1 parent be62d5e commit f932a0c

42 files changed

Lines changed: 1558 additions & 252 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ jobs:
5252
- name: Run vet
5353
run: go vet ./...
5454

55+
- name: Run race tests
56+
run: go test -race ./cmd ./pkg/session ./pkg/accesspolicy ./pkg/publicauth ./pkg/k8s ./pkg/clusterconnect ./pkg/tunnel ./pkg/mesh ./pkg/daemon
57+
58+
- name: Install gosec
59+
run: go install github.com/securego/gosec/v2/cmd/gosec@v2.22.8
60+
61+
- name: Run gosec
62+
run: $(go env GOPATH)/bin/gosec ./cmd ./pkg/... ./assets ./
63+
64+
- name: Verify module files are tidy
65+
run: go mod tidy -diff
66+
5567
- name: Check npm package builder syntax
5668
run: node --check scripts/build-npm-packages.mjs
5769

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 2
33

44
before:
55
hooks:
6-
- go mod tidy
6+
- go mod tidy -diff
77

88
builds:
99
- env:

cmd/apply.go

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"io"
910
"os"
@@ -143,28 +144,35 @@ func runApplyConfig(ctx context.Context, config *applyFile, dryRun bool) ([]appl
143144
return nil, fmt.Errorf("failed to init k8s client: %w", err)
144145
}
145146

146-
results := make([]applyResult, 0, len(config.Tunnels))
147+
tunnelIDs := make([]string, 0, len(config.Tunnels))
147148
for _, item := range config.Tunnels {
148-
result, err := applyOneTunnel(ctx, item, authData, client, kubeconfig, dryRun)
149+
tunnelID, err := applyTunnelID(item.Name)
149150
if err != nil {
150-
rollbackApplyResults(client, results)
151-
return results, err
151+
return nil, err
152152
}
153-
results = append(results, result)
153+
tunnelIDs = append(tunnelIDs, tunnelID)
154154
}
155-
if !dryRun {
155+
156+
results := make([]applyResult, 0, len(config.Tunnels))
157+
err = withTunnelOperationLocks(tunnelIDs, func() error {
158+
for _, item := range config.Tunnels {
159+
result, applyErr := applyOneTunnel(ctx, item, authData, client, kubeconfig, false)
160+
if applyErr != nil {
161+
return applyErrorWithRollback(applyErr, rollbackApplyResults(client, results))
162+
}
163+
results = append(results, result)
164+
}
156165
if err := ensureDaemonRunningFn(); err != nil {
157-
rollbackApplyResults(client, results)
158-
return results, fmt.Errorf("failed to start local daemon: %w", err)
166+
return applyErrorWithRollback(fmt.Errorf("failed to start local daemon: %w", err), rollbackApplyResults(client, results))
159167
}
160168
for _, result := range results {
161169
if err := waitForDaemonSession(result.TunnelID, daemonConnectTimeout); err != nil {
162-
rollbackApplyResults(client, results)
163-
return results, err
170+
return applyErrorWithRollback(err, rollbackApplyResults(client, results))
164171
}
165172
}
166-
}
167-
return results, nil
173+
return nil
174+
})
175+
return results, err
168176
}
169177

170178
func loadApplyFile(path string) (*applyFile, error) {
@@ -253,14 +261,22 @@ func applyOneTunnel(ctx context.Context, item applyTunnel, authData *auth.AuthDa
253261
existing, err := session.Get(normalized.TunnelID)
254262
if err == nil {
255263
alreadyExisted = true
256-
existingSession = existing
257264
currentNamespace := ""
258265
if client != nil {
259266
currentNamespace = client.Namespace()
260267
}
261268
if err := validateExistingApplySessionScope(*existing, authData, currentNamespace); err != nil {
262269
return result, err
263270
}
271+
if client != nil {
272+
if err := refreshSessionFromRemoteLocked(ctx, existing); err != nil {
273+
return result, fmt.Errorf("tunnel %s: sync existing remote state: %w", normalized.TunnelID, err)
274+
}
275+
}
276+
if strings.TrimSpace(existing.Secret) == "" {
277+
return result, fmt.Errorf("tunnel %s already exists but its local secret is unavailable; stop or cleanup the old session before apply", existing.TunnelID)
278+
}
279+
existingSession = existing
264280
if existing.Secret != "" {
265281
secret = existing.Secret
266282
}
@@ -447,9 +463,6 @@ func validateExistingApplySessionScope(existing session.TunnelSession, authData
447463
return fmt.Errorf("tunnel %s already belongs to namespace %s; current namespace is %s", existing.TunnelID, existing.Namespace, currentNamespace)
448464
}
449465
}
450-
if strings.TrimSpace(existing.Secret) == "" {
451-
return fmt.Errorf("tunnel %s already exists but its local secret is unavailable; stop or cleanup the old session before apply", existing.TunnelID)
452-
}
453466
return nil
454467
}
455468

@@ -488,7 +501,8 @@ func rollbackExistingApplyTunnel(client *k8s.Client, previous session.TunnelSess
488501
return firstErr
489502
}
490503

491-
func rollbackApplyResults(client *k8s.Client, results []applyResult) {
504+
func rollbackApplyResults(client *k8s.Client, results []applyResult) error {
505+
var rollbackErrors []error
492506
for i := len(results) - 1; i >= 0; i-- {
493507
result := results[i]
494508
if result.TunnelID == "" {
@@ -497,16 +511,30 @@ func rollbackApplyResults(client *k8s.Client, results []applyResult) {
497511
if result.NewTunnel {
498512
if client != nil {
499513
cleanupCtx, cancel := context.WithTimeout(context.Background(), tunnelCleanupTimeout)
500-
_ = client.CleanupTunnel(cleanupCtx, result.TunnelID)
514+
if err := client.CleanupTunnel(cleanupCtx, result.TunnelID); err != nil {
515+
rollbackErrors = append(rollbackErrors, fmt.Errorf("cleanup tunnel %s: %w", result.TunnelID, err))
516+
}
501517
cancel()
502518
}
503-
_ = session.Delete(result.TunnelID)
519+
if err := session.Delete(result.TunnelID); err != nil {
520+
rollbackErrors = append(rollbackErrors, fmt.Errorf("delete local session %s: %w", result.TunnelID, err))
521+
}
504522
continue
505523
}
506524
if result.Previous != nil {
507-
_ = rollbackExistingApplyTunnel(client, *result.Previous)
525+
if err := rollbackExistingApplyTunnel(client, *result.Previous); err != nil {
526+
rollbackErrors = append(rollbackErrors, fmt.Errorf("restore tunnel %s: %w", result.TunnelID, err))
527+
}
508528
}
509529
}
530+
return errors.Join(rollbackErrors...)
531+
}
532+
533+
func applyErrorWithRollback(applyErr, rollbackErr error) error {
534+
if rollbackErr == nil {
535+
return applyErr
536+
}
537+
return errors.Join(applyErr, fmt.Errorf("rollback failed: %w", rollbackErr))
510538
}
511539

512540
func normalizeApplyTunnel(item applyTunnel) (normalizedApplyTunnel, error) {
@@ -763,6 +791,9 @@ func applyTunnelID(name string) (string, error) {
763791
if name != strings.ToLower(name) || !applyNamePattern.MatchString(name) {
764792
return "", fmt.Errorf("invalid tunnel name %q: use lowercase DNS-compatible names, e.g. web or api-dev", name)
765793
}
794+
if strings.HasPrefix(name, "mesh-") {
795+
return "", fmt.Errorf("invalid tunnel name %q: the mesh- prefix is reserved for Sealtun Mesh resources", name)
796+
}
766797
return name, nil
767798
}
768799

cmd/apply_test.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"context"
5+
"errors"
56
"os"
67
"path/filepath"
78
"strings"
@@ -135,6 +136,15 @@ func TestNormalizeApplyTunnelRejectsUnsafeNames(t *testing.T) {
135136
}
136137
}
137138

139+
func TestApplyTunnelIDRejectsMeshReservedPrefix(t *testing.T) {
140+
t.Parallel()
141+
142+
_, err := applyTunnelID("mesh-global")
143+
if err == nil || !strings.Contains(err.Error(), "reserved") {
144+
t.Fatalf("expected reserved mesh prefix error, got %v", err)
145+
}
146+
}
147+
138148
func TestNormalizeApplyTunnelDefaultsProtocol(t *testing.T) {
139149
t.Parallel()
140150

@@ -903,7 +913,7 @@ func TestRollbackApplyResultsRestoresExistingLocalSession(t *testing.T) {
903913
t.Fatal(err)
904914
}
905915

906-
rollbackApplyResults(nil, []applyResult{{
916+
if err := rollbackApplyResults(nil, []applyResult{{
907917
TunnelID: "web",
908918
Previous: &session.TunnelSession{
909919
TunnelID: previous.TunnelID,
@@ -917,7 +927,9 @@ func TestRollbackApplyResultsRestoresExistingLocalSession(t *testing.T) {
917927
Secret: previous.Secret,
918928
Mode: previous.Mode,
919929
},
920-
}})
930+
}}); err != nil {
931+
t.Fatalf("rollbackApplyResults returned error: %v", err)
932+
}
921933

922934
current, err := session.Get("web")
923935
if err != nil {
@@ -940,3 +952,52 @@ func TestRollbackApplyResultsRestoresExistingLocalSession(t *testing.T) {
940952
}
941953
}
942954
}
955+
956+
func TestApplyOneTunnelPropagatesRemoteRefreshFailure(t *testing.T) {
957+
t.Setenv("SEALTUN_HOME", t.TempDir())
958+
if err := session.Save(session.TunnelSession{
959+
TunnelID: "web",
960+
Region: "https://gzg.sealos.run",
961+
Namespace: "default",
962+
LocalPort: "3000",
963+
Protocol: "https",
964+
Secret: "secret",
965+
}); err != nil {
966+
t.Fatal(err)
967+
}
968+
969+
want := errors.New("remote state unavailable")
970+
previousCollect := collectSessionRemoteState
971+
collectSessionRemoteState = func(context.Context, session.TunnelSession) (*k8s.TunnelRemoteState, error) {
972+
return nil, want
973+
}
974+
t.Cleanup(func() { collectSessionRemoteState = previousCollect })
975+
976+
_, err := applyOneTunnel(context.Background(), applyTunnel{Name: "web", LocalPort: 3000}, &auth.AuthData{Region: "https://gzg.sealos.run"}, &k8s.Client{}, "", false)
977+
if !errors.Is(err, want) {
978+
t.Fatalf("apply error = %v, want %v", err, want)
979+
}
980+
}
981+
982+
func TestRollbackApplyResultsReturnsLocalRestoreFailure(t *testing.T) {
983+
home := t.TempDir()
984+
t.Setenv("SEALTUN_HOME", home)
985+
if err := os.WriteFile(filepath.Join(home, ".sealtun"), []byte("not a directory"), 0o600); err != nil {
986+
t.Fatal(err)
987+
}
988+
989+
err := rollbackApplyResults(nil, []applyResult{{
990+
TunnelID: "web",
991+
Previous: &session.TunnelSession{
992+
TunnelID: "web",
993+
Region: "https://gzg.sealos.run",
994+
Namespace: "default",
995+
LocalPort: "3000",
996+
Protocol: "https",
997+
Secret: "secret",
998+
},
999+
}})
1000+
if err == nil || !strings.Contains(err.Error(), "restore tunnel web") {
1001+
t.Fatalf("expected rollback failure to be reported, got %v", err)
1002+
}
1003+
}

cmd/daemon.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,22 @@ var daemonCmd = &cobra.Command{
6262
continue
6363
}
6464
if sessionExpired(sess, time.Now()) {
65-
fmt.Printf("[+] tunnel %s expired; cleaning up...\n", sess.TunnelID)
66-
cleanupCtx, cancel := context.WithTimeout(ctx, tunnelCleanupTimeout)
67-
err := cleanupSessionResources(cleanupCtx, sess)
68-
cancel()
65+
latest, removed, err := cleanupExpiredDaemonSession(ctx, sess.TunnelID)
6966
if err != nil {
7067
fmt.Printf("[!] expired tunnel %s cleanup failed: %v\n", sess.TunnelID, err)
7168
continue
7269
}
73-
if err := session.Delete(sess.TunnelID); err != nil && !os.IsNotExist(err) {
74-
fmt.Printf("[!] expired tunnel %s local cleanup failed: %v\n", sess.TunnelID, err)
70+
if removed {
71+
fmt.Printf("[+] expired tunnel %s cleaned up\n", sess.TunnelID)
72+
continue
73+
}
74+
if latest == nil {
75+
continue
76+
}
77+
sess = *latest
78+
if sess.Mode != "daemon" {
79+
continue
7580
}
76-
continue
7781
}
7882
if sess.ConnectionState == session.ConnectionStateStopped {
7983
continue
@@ -108,6 +112,34 @@ var daemonCmd = &cobra.Command{
108112
},
109113
}
110114

115+
func cleanupExpiredDaemonSession(ctx context.Context, tunnelID string) (latest *session.TunnelSession, removed bool, err error) {
116+
err = withTunnelOperationLockContext(ctx, tunnelID, func() error {
117+
current, getErr := session.Get(tunnelID)
118+
if os.IsNotExist(getErr) {
119+
return nil
120+
}
121+
if getErr != nil {
122+
return getErr
123+
}
124+
if current.Mode != "daemon" || !sessionExpired(*current, time.Now()) {
125+
latest = current
126+
return nil
127+
}
128+
129+
cleanupCtx, cancel := context.WithTimeout(ctx, tunnelCleanupTimeout)
130+
defer cancel()
131+
if cleanupErr := cleanupSessionResources(cleanupCtx, *current); cleanupErr != nil {
132+
return cleanupErr
133+
}
134+
if deleteErr := session.Delete(tunnelID); deleteErr != nil && !os.IsNotExist(deleteErr) {
135+
return fmt.Errorf("delete local session: %w", deleteErr)
136+
}
137+
removed = true
138+
return nil
139+
})
140+
return latest, removed, err
141+
}
142+
111143
func reconcileDaemonWorkers(
112144
ctx context.Context,
113145
mu *sync.Mutex,

0 commit comments

Comments
 (0)