diff --git a/src/pkg/cli/client/byoc/aws/byoc.go b/src/pkg/cli/client/byoc/aws/byoc.go index 84f3098f3..9c06d4f02 100644 --- a/src/pkg/cli/client/byoc/aws/byoc.go +++ b/src/pkg/cli/client/byoc/aws/byoc.go @@ -3,7 +3,9 @@ package aws import ( "bytes" "context" + "crypto/sha256" "encoding/base64" + "encoding/binary" "encoding/json" "errors" "fmt" @@ -105,7 +107,12 @@ func (b *ByocAws) setUp(ctx context.Context) error { term.Debug("Failed to get subdomain zone:", err) // return err; FIXME: ignore this error for now } else { - b.ProjectDomain = b.getProjectDomain(domain.Zone) + whoami, err := b.WhoAmI(ctx) + if err != nil { + return err + } + + b.ProjectDomain = b.getProjectDomain(whoami.Account, domain.Zone) if b.ProjectDomain != "" { b.ShouldDelegateSubdomain = true } @@ -468,7 +475,7 @@ func (b *ByocAws) Follow(ctx context.Context, req *defangv1.TailRequest) (client var err error var taskArn ecs.TaskArn var eventStream ecs.EventStream - if etag != "" && !pkg.IsValidRandomID(etag) { + if etag != "" && !pkg.IsValidBase36ID(etag) { // Assume "etag" is a task ID eventStream, err = b.driver.TailTaskID(ctx, etag) taskArn, _ = b.driver.GetTaskArn(etag) @@ -639,15 +646,14 @@ func (b *ByocAws) getPrivateFqdn(fqn qualifiedName) string { return fmt.Sprintf("%s.%s", safeFqn, b.PrivateDomain) // TODO: consider merging this with ServiceDNS } -func (b *ByocAws) getProjectDomain(zone string) string { +func (b *ByocAws) getProjectDomain(account, zone string) string { if b.ProjectName == "" { return "" // no project name => no custom domain } - projectLabel := byoc.DnsSafeLabel(b.ProjectName) - if projectLabel == byoc.DnsSafeLabel(b.TenantID) { - return byoc.DnsSafe(zone) // the zone will already have the tenant ID - } - return projectLabel + "." + byoc.DnsSafe(zone) + h := sha256.New() + fmt.Fprintf(h, "%s.%s.%s.%s.%s", account, b.ProjectName, b.PulumiStack, b.TenantID, zone) + + return pkg.Base36ID(binary.LittleEndian.Uint64(h.Sum(nil)[:8])) + "." + byoc.DnsSafe(zone) } func (b *ByocAws) TearDown(ctx context.Context) error { diff --git a/src/pkg/cli/client/byoc/aws/byoc_test.go b/src/pkg/cli/client/byoc/aws/byoc_test.go index d06f8ee1f..73bf80a59 100644 --- a/src/pkg/cli/client/byoc/aws/byoc_test.go +++ b/src/pkg/cli/client/byoc/aws/byoc_test.go @@ -32,17 +32,17 @@ func TestDomainMultipleProjectSupport(t *testing.T) { PublicFqdn string PrivateFqdn string }{ - {"tenant1", "tenant1", "web", port80, "web--80.example.com", "web.example.com", "web.tenant1.internal"}, - {"tenant1", "tenant1", "web", hostModePort, "web.tenant1.internal:80", "web.example.com", "web.tenant1.internal"}, - {"project1", "tenant1", "web", port80, "web--80.project1.example.com", "web.project1.example.com", "web.project1.internal"}, - {"Project1", "tenant1", "web", port80, "web--80.project1.example.com", "web.project1.example.com", "web.project1.internal"}, - {"project1", "tenant1", "web", hostModePort, "web.project1.internal:80", "web.project1.example.com", "web.project1.internal"}, - {"project1", "tenant1", "api", port8080, "api--8080.project1.example.com", "api.project1.example.com", "api.project1.internal"}, - {"tenant1", "tenant1", "web", port80, "web--80.example.com", "web.example.com", "web.tenant1.internal"}, - {"tenant1", "tenant1", "web", hostModePort, "web.tenant1.internal:80", "web.example.com", "web.tenant1.internal"}, - {"Project1", "tenant1", "web", port80, "web--80.project1.example.com", "web.project1.example.com", "web.project1.internal"}, - {"Tenant2", "tenant1", "web", port80, "web--80.tenant2.example.com", "web.tenant2.example.com", "web.tenant2.internal"}, - {"tenant1", "tenAnt1", "web", port80, "web--80.example.com", "web.example.com", "web.tenant1.internal"}, + {"tenant1", "tenant1", "web", port80, `web--80.hrdsvwhcn3jj.example.com`, `web.hrdsvwhcn3jj.example.com`, "web.tenant1.internal"}, + {"tenant1", "tenant1", "web", hostModePort, `web.tenant1.internal:80`, `web.hrdsvwhcn3jj.example.com`, "web.tenant1.internal"}, + {"project1", "tenant1", "web", port80, `web--80.b9vgnfzbeoeu.example.com`, `web.b9vgnfzbeoeu.example.com`, "web.project1.internal"}, + {"Project1", "tenant1", "web", port80, `web--80.cr8i4dwuk1vs.example.com`, `web.cr8i4dwuk1vs.example.com`, "web.project1.internal"}, + {"project1", "tenant1", "web", hostModePort, `web.project1.internal:80`, `web.b9vgnfzbeoeu.example.com`, "web.project1.internal"}, + {"project1", "tenant1", "api", port8080, `api--8080.b9vgnfzbeoeu.example.com`, `api.b9vgnfzbeoeu.example.com`, "api.project1.internal"}, + {"tenant1", "tenant1", "web", port80, `web--80.hrdsvwhcn3jj.example.com`, `web.hrdsvwhcn3jj.example.com`, "web.tenant1.internal"}, + {"tenant1", "tenant1", "web", hostModePort, `web.tenant1.internal:80`, `web.hrdsvwhcn3jj.example.com`, "web.tenant1.internal"}, + {"Project1", "tenant1", "web", port80, `web--80.cr8i4dwuk1vs.example.com`, `web.cr8i4dwuk1vs.example.com`, "web.project1.internal"}, + {"Tenant2", "tenant1", "web", port80, `web--80.wsm43awbq8pw.example.com`, `web.wsm43awbq8pw.example.com`, "web.tenant2.internal"}, + {"tenant1", "tenAnt1", "web", port80, `web--80.b63ocx2b6o4t.example.com`, `web.b63ocx2b6o4t.example.com`, "web.tenant1.internal"}, } for _, tt := range tests { @@ -52,15 +52,15 @@ func TestDomainMultipleProjectSupport(t *testing.T) { if _, err := b.LoadProject(context.Background()); err != nil { t.Fatalf("LoadProject() failed: %v", err) } - b.ProjectDomain = b.getProjectDomain("example.com") + b.ProjectDomain = b.getProjectDomain("123456789012", "example.com") endpoint := b.getEndpoint(tt.Fqn, tt.Port) - if endpoint != tt.EndPoint { + if tt.EndPoint != endpoint { t.Errorf("expected endpoint %q, got %q", tt.EndPoint, endpoint) } publicFqdn := b.getPublicFqdn(tt.Fqn) - if publicFqdn != tt.PublicFqdn { + if tt.PublicFqdn != publicFqdn { t.Errorf("expected public fqdn %q, got %q", tt.PublicFqdn, publicFqdn) } diff --git a/src/pkg/cli/client/byoc/aws/stream.go b/src/pkg/cli/client/byoc/aws/stream.go index 115abede8..29f8904cd 100644 --- a/src/pkg/cli/client/byoc/aws/stream.go +++ b/src/pkg/cli/client/byoc/aws/stream.go @@ -104,7 +104,7 @@ func (bs *byocServerStream) parseEvents(events []ecs.LogEvent) (*defangv1.TailRe // These events are from an awslogs service task: "tenant/service_etag/taskID" stream response.Host = parts[2] // TODO: figure out actual hostname/IP parts = strings.Split(parts[1], "_") - if len(parts) != 2 || !pkg.IsValidRandomID(parts[1]) { + if len(parts) != 2 || !pkg.IsValidBase36ID(parts[1]) { // skip, ignore sidecar logs (like route53-sidecar or fluentbit) return nil, nil } diff --git a/src/pkg/cli/tail.go b/src/pkg/cli/tail.go index 9d85df842..8b7690bd3 100644 --- a/src/pkg/cli/tail.go +++ b/src/pkg/cli/tail.go @@ -8,6 +8,7 @@ import ( "os" "regexp" "strings" + "syscall" "time" "github.com/DefangLabs/defang/src/pkg" @@ -177,12 +178,26 @@ func Tail(ctx context.Context, client client.Client, params TailOptions) error { } func isTransientError(err error) bool { + if errors.Is(err, io.ErrUnexpectedEOF) { + return true + } + + // Consider connection reset error as transient + if errors.Is(err, syscall.ECONNRESET) { + return true + } + + // Network timeouts are transient, not using Temporary() because it's not always accurate + // See https://pkg.go.dev/net#Error + if neterr, ok := err.(interface{ Timeout() bool }); ok && neterr.Timeout() { + return true + } + // TODO: detect ALB timeout (504) or Fabric restart and reconnect automatically code := connect.CodeOf(err) // Reconnect on Error: internal: stream error: stream ID 5; INTERNAL_ERROR; received from peer return code == connect.CodeUnavailable || - (code == connect.CodeInternal && !connect.IsWireError(err)) || - errors.Is(err, io.ErrUnexpectedEOF) + (code == connect.CodeInternal && !connect.IsWireError(err)) } diff --git a/src/pkg/utils.go b/src/pkg/utils.go index 8b8f812ef..5ac4ca765 100644 --- a/src/pkg/utils.go +++ b/src/pkg/utils.go @@ -66,15 +66,19 @@ func (l *OneOrList) UnmarshalJSON(data []byte) error { } func RandomID() string { - const uint64msb = 1 << 63 // always set the MSB to ensure we get ≥12 digits - return strconv.FormatUint(rand.Uint64()|uint64msb, 36)[1:] + return Base36ID(rand.Uint64()) } -func IsValidRandomID(s string) bool { +func IsValidBase36ID(s string) bool { _, err := strconv.ParseUint(s, 36, 64) return len(s) == 12 && err == nil } +func Base36ID(i uint64) string { + const uint64msb = 1 << 63 // always set the MSB to ensure we get ≥12 digits + return strconv.FormatUint(i|uint64msb, 36)[1:] +} + func Min(a, b int) int { if a < b { return a diff --git a/src/pkg/utils_test.go b/src/pkg/utils_test.go index e2ee0da60..8e7f68c6a 100644 --- a/src/pkg/utils_test.go +++ b/src/pkg/utils_test.go @@ -130,7 +130,7 @@ func TestRandomID(t *testing.T) { t.Errorf("RandomID() = %v, want unique ID", id) } unique[id] = true - if !IsValidRandomID(id) { + if !IsValidBase36ID(id) { t.Errorf("RandomID() = %v, want IsValidRandomID true", id) } }