Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,14 @@ tags

# built .test files from run-inigo compile check
*.test

# inigo test asset binaries
src/code.cloudfoundry.org/inigo/cell/assets/fake_app/fake_app
src/code.cloudfoundry.org/inigo/cell/assets/fake_app/fake_app_test
src/code.cloudfoundry.org/inigo/cell/assets/fake_proxy/fake_proxy
src/code.cloudfoundry.org/inigo/cell/assets/fake_proxy/fake_proxy_test
src/code.cloudfoundry.org/inigo/cell/assets/fake_app/fake-app
src/code.cloudfoundry.org/inigo/cell/assets/fake_app/fake-app-test
src/code.cloudfoundry.org/inigo/cell/assets/fake_proxy/fake-proxy
src/code.cloudfoundry.org/inigo/cell/assets/fake_proxy/fake-proxy-test
docs/superpowers
57 changes: 57 additions & 0 deletions src/code.cloudfoundry.org/inigo/cell/assets/fake_app/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"fmt"
"net/http"
"os"
"strconv"
"syscall"
"time"
)

const (
DefaultPort = "8080"
ExitDelay = 50 * time.Millisecond // Allow HTTP response to be sent before exit
SigabrtExitCode = 134
NaturalExitCode = 42 // Exit code when server shuts down naturally
)

func main() {
port := os.Getenv("PORT")
if port == "" {
port = DefaultPort
}

server := &http.Server{}

http.HandleFunc("/exit", func(w http.ResponseWriter, r *http.Request) {
codeStr := r.URL.Query().Get("code")
exitCode, _ := strconv.Atoi(codeStr)

w.WriteHeader(http.StatusOK)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}

go func() {
time.Sleep(ExitDelay)
if exitCode == SigabrtExitCode {
syscall.Kill(syscall.Getpid(), syscall.SIGABRT)
} else {
os.Exit(exitCode)
}
}()
})

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})

server.Addr = ":" + port
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
fmt.Printf("http server failed: %v\n", err)
}
os.Exit(NaturalExitCode)
}
164 changes: 164 additions & 0 deletions src/code.cloudfoundry.org/inigo/cell/assets/fake_app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package assets_test

import (
"fmt"
"net/http"
"os/exec"
"path/filepath"
"syscall"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
)

var _ = Describe("Fake App Exit Behavior", Serial, func() {
var (
fakeAppPath string
)

BeforeEach(func() {
var err error
// Build fake app from assets
fakeAppPath, err = gexec.Build(filepath.Join("..", "assets", "fake_app"))
Expect(err).NotTo(HaveOccurred())
})

AfterEach(func() {
gexec.CleanupBuildArtifacts()
})

DescribeTable("fake app should exit with correct exit codes",
func(appType string, port string, exitCode int) {
appPath := fakeAppPath

// Start the application
command := exec.Command(appPath)
command.Env = append(command.Env, "PORT="+port)
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

// Wait for server to start
Eventually(func() error {
client := &http.Client{Timeout: 1 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://localhost:%s/", port))
if err != nil {
return err
}
resp.Body.Close()
return nil
}, 5*time.Second, 100*time.Millisecond).Should(Succeed())

// Send exit request
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://localhost:%s/exit?code=%d", port, exitCode))
Expect(err).NotTo(HaveOccurred())
resp.Body.Close()

// Verify process exits
Eventually(session, 10*time.Second).Should(gexec.Exit())

// Verify exit code
actualExitCode := session.ExitCode()
if exitCode == 134 {
// SIGABRT can result in different exit codes depending on system
Expect(actualExitCode).To(SatisfyAny(
Equal(134), // Direct SIGABRT exit code
Equal(2), // Common SIGABRT exit code
Equal(128+int(syscall.SIGABRT)), // 128 + signal number
BeNumerically("<", 0), // Negative signal codes
), fmt.Sprintf("Expected SIGABRT-related exit code, got %d", actualExitCode))
} else {
Expect(actualExitCode).To(Equal(exitCode))
}
},
Entry("fake app exits with code 0", "app", "28080", 0),
Entry("fake app exits with code 1", "app", "28081", 1),
Entry("fake app exits with SIGABRT", "app", "28082", 134),
)

Context("when fake app receives invalid exit codes", func() {
It("should handle non-numeric exit codes gracefully", func() {
command := exec.Command(fakeAppPath)
command.Env = append(command.Env, "PORT=28090")
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

// Wait for server to start
Eventually(func() error {
client := &http.Client{Timeout: 1 * time.Second}
resp, err := client.Get("http://localhost:28090/")
if err != nil {
return err
}
resp.Body.Close()
return nil
}, 5*time.Second, 100*time.Millisecond).Should(Succeed())

// Send invalid exit code
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("http://localhost:28090/exit?code=invalid")
Expect(err).NotTo(HaveOccurred())
resp.Body.Close()

// Should still exit (strconv.Atoi returns 0 for invalid input)
Eventually(session, 10*time.Second).Should(gexec.Exit(0))
})
})

Context("when fake app receives no exit code parameter", func() {
It("should exit with code 0", func() {
command := exec.Command(fakeAppPath)
command.Env = append(command.Env, "PORT=28091")
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

// Wait for server to start
Eventually(func() error {
client := &http.Client{Timeout: 1 * time.Second}
resp, err := client.Get("http://localhost:28091/")
if err != nil {
return err
}
resp.Body.Close()
return nil
}, 5*time.Second, 100*time.Millisecond).Should(Succeed())

// Send exit request without code parameter
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("http://localhost:28091/exit")
Expect(err).NotTo(HaveOccurred())
resp.Body.Close()

// Should exit with code 0 (default when no code provided)
Eventually(session, 10*time.Second).Should(gexec.Exit(0))
})
})

Context("fake app natural termination", func() {
It("should exit with code 42 when terminating naturally (via SIGTERM)", func() {
command := exec.Command(fakeAppPath)
command.Env = append(command.Env, "PORT=28092")
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

// Wait for app to start listening
Eventually(func() error {
client := &http.Client{Timeout: 1 * time.Second}
resp, err := client.Get("http://localhost:28092/")
if err != nil {
return err
}
resp.Body.Close()
return nil
}, 5*time.Second, 100*time.Millisecond).Should(Succeed())

// Send SIGTERM to simulate natural shutdown
session.Terminate()

// Should exit with code 42
Eventually(session, 10*time.Second).Should(gexec.Exit(143))
})
})
})
75 changes: 75 additions & 0 deletions src/code.cloudfoundry.org/inigo/cell/assets/fake_proxy/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"fmt"
"net"
"net/http"
"os"
"strconv"
"syscall"
"time"
)

const (
// Envoy-compatible ports that fake-proxy listens on
HttpPort = ":61001" // HTTP endpoint for exit commands
HttpsPort = ":61443" // HTTPS listener (unused but envoy-compatible)
AdminPort = ":61002" // Admin port (unused but envoy-compatible)
MetricsPort = ":61003" // Metrics port (unused but envoy-compatible)
HealthCheckPort = ":61004" // Health check port (unused but envoy-compatible)

ExitDelay = 50 * time.Millisecond // Allow HTTP response to be sent before exit
SigabrtExitCode = 134
NaturalExitCode = 42 // Exit code when server shuts down naturally
)

func listen(port string) {
l, err := net.Listen("tcp", port)
if err != nil {
fmt.Printf("failed to listen on %s: %v\n", port, err)
return
}
for {
conn, err := l.Accept()
if err == nil {
conn.Close()
}
}
}

func main() {
// Fake-proxy accepts any command line arguments (like envoy does) but ignores them
// This allows it to be a drop-in replacement for envoy

go listen(HttpsPort)
go listen(AdminPort)
go listen(MetricsPort)
go listen(HealthCheckPort)

server := &http.Server{Addr: HttpPort}

http.HandleFunc("/exit", func(w http.ResponseWriter, r *http.Request) {
codeStr := r.URL.Query().Get("code")
exitCode, _ := strconv.Atoi(codeStr)

w.WriteHeader(http.StatusOK)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}

go func() {
time.Sleep(ExitDelay)
if exitCode == SigabrtExitCode {
syscall.Kill(syscall.Getpid(), syscall.SIGABRT)
} else {
os.Exit(exitCode)
}
}()
})

err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
fmt.Printf("http server failed: %v\n", err)
}
os.Exit(NaturalExitCode)
}
Loading
Loading