From 1053a58a01f80d4921e23fa312e004f86e51c378 Mon Sep 17 00:00:00 2001 From: ramonskie Date: Wed, 17 Dec 2025 16:41:09 +0100 Subject: [PATCH] Add RuntimeLogs() method for retrieving application runtime logs Provides a platform-agnostic API for retrieving runtime logs from deployed applications after staging completes. This complements the existing staging logs returned by platform.Deploy.Execute() by adding access to post-deployment application output. This change also refactors the log fetching logic introduced in commit 8fb0d65 by extracting a shared FetchRecentLogs() helper function, eliminating code duplication between staging failure logs and runtime logs retrieval. Key additions: - Add Deployment.RuntimeLogs() method for runtime log retrieval - Extract cloudfoundry.FetchRecentLogs() shared helper function - Add comprehensive tests for both CloudFoundry and Docker platforms - Update README with RuntimeLogs() usage examples Benefits: - Clear separation between staging logs (build-time) and runtime logs - Tests can verify application startup, service connections, module loading - Simplifies buildpack integration tests (no manual cf/docker logs commands) - Consolidates log fetching logic (DRY principle) - Self-documenting API with explicit method names Use cases: - Testing application startup messages - Verifying service connection logs (Redis, PostgreSQL, etc.) - Checking module/extension loading (PHP extensions, npm packages) - Validating runtime configuration - Debugging runtime errors Example usage: deployment, stagingLogs, err := platform.Deploy.Execute(name, path) Expect(err).NotTo(HaveOccurred()) // stagingLogs contains build-time output Expect(stagingLogs).To(ContainSubstring("Installing dependencies")) // RuntimeLogs() retrieves application output runtimeLogs, err := deployment.RuntimeLogs() Expect(err).NotTo(HaveOccurred()) Expect(runtimeLogs).To(ContainSubstring("Application started")) --- README.md | 25 +++++++++++ cloudfoundry.go | 8 +++- cloudfoundry_test.go | 50 ++++++++++++++++++--- deployment.go | 70 ++++++++++++++++++++++++++++++ docker.go | 13 +++--- docker_test.go | 45 ++++++++++++++++--- fakes/logs_client.go | 39 +++++++++++++++++ internal/cloudfoundry/init_test.go | 1 + internal/cloudfoundry/logs.go | 28 ++++++++++++ internal/cloudfoundry/logs_test.go | 60 +++++++++++++++++++++++++ internal/cloudfoundry/stage.go | 12 ++--- platform.go | 16 +++---- 12 files changed, 331 insertions(+), 36 deletions(-) create mode 100644 fakes/logs_client.go create mode 100644 internal/cloudfoundry/logs.go create mode 100644 internal/cloudfoundry/logs_test.go diff --git a/README.md b/README.md index 1fc0e6e..f7e4623 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,31 @@ deployment, logs, err := platform.Deploy. Execute("my-app", "/path/to/my/app/source") ``` +### Retrieving runtime logs: `RuntimeLogs` + +The `deployment.RuntimeLogs()` method retrieves logs from the running application +after deployment succeeds. This is useful for testing runtime behavior such as +application startup, service connections, and module loading. + +```go +// Deploy an application +deployment, stagingLogs, err := platform.Deploy.Execute("my-app", "/path/to/my/app/source") +Expect(err).NotTo(HaveOccurred()) + +// stagingLogs contains build-time output (buildpack detection, compilation, etc.) +Expect(stagingLogs).To(ContainLines(ContainSubstring("Installing dependencies..."))) + +// Retrieve runtime logs (application startup, service connections, etc.) +runtimeLogs, err := deployment.RuntimeLogs() +Expect(err).NotTo(HaveOccurred()) +Expect(runtimeLogs).To(ContainSubstring("Application started")) +Expect(runtimeLogs).To(ContainSubstring("Connected to Redis")) +``` + +**Note:** The logs returned from `platform.Deploy.Execute()` are **staging logs** +(build-time), while `deployment.RuntimeLogs()` returns **runtime logs** (post-deployment). +Use staging logs to test buildpack behavior, and runtime logs to test application behavior. + ## Other utilities ### Random name generation: `RandomName` diff --git a/cloudfoundry.go b/cloudfoundry.go index 30bb055..53b34f0 100644 --- a/cloudfoundry.go +++ b/cloudfoundry.go @@ -14,11 +14,11 @@ import ( //go:generate faux --package github.com/cloudfoundry/switchblade/internal/cloudfoundry --interface StagePhase --name CloudFoundryStagePhase --output fakes/cloudfoundry_stage_phase.go //go:generate faux --package github.com/cloudfoundry/switchblade/internal/cloudfoundry --interface TeardownPhase --name CloudFoundryTeardownPhase --output fakes/cloudfoundry_teardown_phase.go -func NewCloudFoundry(initialize cloudfoundry.InitializePhase, deinitialize cloudfoundry.DeinitializePhase, setup cloudfoundry.SetupPhase, stage cloudfoundry.StagePhase, teardown cloudfoundry.TeardownPhase, workspace string) Platform { +func NewCloudFoundry(initialize cloudfoundry.InitializePhase, deinitialize cloudfoundry.DeinitializePhase, setup cloudfoundry.SetupPhase, stage cloudfoundry.StagePhase, teardown cloudfoundry.TeardownPhase, workspace string, cli cloudfoundry.Executable) Platform { return Platform{ initialize: cloudFoundryInitializeProcess{initialize: initialize}, deinitialize: cloudFoundryDeinitializeProcess{deinitialize: deinitialize}, - Deploy: cloudFoundryDeployProcess{setup: setup, stage: stage, workspace: workspace}, + Deploy: cloudFoundryDeployProcess{setup: setup, stage: stage, workspace: workspace, cli: cli}, Delete: cloudFoundryDeleteProcess{teardown: teardown, workspace: workspace}, } } @@ -51,6 +51,7 @@ type cloudFoundryDeployProcess struct { setup cloudfoundry.SetupPhase stage cloudfoundry.StagePhase workspace string + cli cloudfoundry.Executable } func (p cloudFoundryDeployProcess) WithBuildpacks(buildpacks ...string) DeployProcess { @@ -111,6 +112,9 @@ func (p cloudFoundryDeployProcess) Execute(name, source string) (Deployment, fmt Name: name, ExternalURL: externalURL, InternalURL: internalURL, + platform: CloudFoundry, + workspace: home, + cfCLI: p.cli, }, logs, nil } diff --git a/cloudfoundry_test.go b/cloudfoundry_test.go index 97dfe7d..a73e615 100644 --- a/cloudfoundry_test.go +++ b/cloudfoundry_test.go @@ -11,6 +11,8 @@ import ( "github.com/cloudfoundry/switchblade" "github.com/cloudfoundry/switchblade/fakes" "github.com/cloudfoundry/switchblade/internal/cloudfoundry" + cffakes "github.com/cloudfoundry/switchblade/internal/cloudfoundry/fakes" + "github.com/paketo-buildpacks/packit/v2/pexec" "github.com/sclevine/spec" . "github.com/cloudfoundry/switchblade/matchers" @@ -26,6 +28,7 @@ func testCloudFoundry(t *testing.T, context spec.G, it spec.S) { setup *fakes.CloudFoundrySetupPhase stage *fakes.CloudFoundryStagePhase teardown *fakes.CloudFoundryTeardownPhase + cli *cffakes.Executable workspace string platform switchblade.Platform @@ -37,12 +40,13 @@ func testCloudFoundry(t *testing.T, context spec.G, it spec.S) { setup = &fakes.CloudFoundrySetupPhase{} stage = &fakes.CloudFoundryStagePhase{} teardown = &fakes.CloudFoundryTeardownPhase{} + cli = &cffakes.Executable{} var err error workspace, err = os.MkdirTemp("", "workspace") Expect(err).NotTo(HaveOccurred()) - platform = switchblade.NewCloudFoundry(initialize, deinitialize, setup, stage, teardown, workspace) + platform = switchblade.NewCloudFoundry(initialize, deinitialize, setup, stage, teardown, workspace, cli) }) it.After(func() { @@ -142,11 +146,9 @@ func testCloudFoundry(t *testing.T, context spec.G, it spec.S) { it("executes the setup and stage phases", func() { deployment, logs, err := platform.Deploy.Execute("some-app", "/some/path/to/my/app") Expect(err).NotTo(HaveOccurred()) - Expect(deployment).To(Equal(switchblade.Deployment{ - Name: "some-app", - ExternalURL: "some-external-url", - InternalURL: "some-internal-url", - })) + Expect(deployment.Name).To(Equal("some-app")) + Expect(deployment.ExternalURL).To(Equal("some-external-url")) + Expect(deployment.InternalURL).To(Equal("some-internal-url")) Expect(logs).To(ContainLines( "Setting up...", "Staging...", @@ -162,6 +164,42 @@ func testCloudFoundry(t *testing.T, context spec.G, it spec.S) { Expect(stage.RunCall.Receives.Name).To(Equal("some-app")) }) + it("retrieves runtime logs from deployed application", func() { + cli.ExecuteCall.Stub = func(execution pexec.Execution) error { + if execution.Args[0] == "logs" { + fmt.Fprintln(execution.Stdout, "Runtime log line 1") + fmt.Fprintln(execution.Stdout, "Application started successfully") + fmt.Fprintln(execution.Stdout, "Runtime log line 3") + } + return nil + } + + deployment, stagingLogs, err := platform.Deploy.Execute("some-app", "/some/path/to/my/app") + Expect(err).NotTo(HaveOccurred()) + + // Staging logs should contain setup/staging output + Expect(stagingLogs.String()).To(ContainSubstring("Setting up...")) + Expect(stagingLogs.String()).To(ContainSubstring("Staging...")) + + // Runtime logs should contain application output + runtimeLogs, err := deployment.RuntimeLogs() + Expect(err).NotTo(HaveOccurred()) + Expect(runtimeLogs).To(ContainSubstring("Runtime log line 1")) + Expect(runtimeLogs).To(ContainSubstring("Application started successfully")) + Expect(runtimeLogs).To(ContainSubstring("Runtime log line 3")) + + // Verify the CLI was called with the correct arguments + var logsCallReceived pexec.Execution + for i := 0; i < cli.ExecuteCall.CallCount; i++ { + if len(cli.ExecuteCall.Receives.Execution.Args) > 0 && cli.ExecuteCall.Receives.Execution.Args[0] == "logs" { + logsCallReceived = cli.ExecuteCall.Receives.Execution + break + } + } + Expect(logsCallReceived.Args).To(Equal([]string{"logs", "some-app", "--recent"})) + Expect(logsCallReceived.Env).To(ContainElement(ContainSubstring("CF_HOME="))) + }) + context("WithBuildpacks", func() { it("uses those buildpacks", func() { platform.Deploy.WithBuildpacks("some-buildpack", "other-buildpack") diff --git a/deployment.go b/deployment.go index 3f0a591..fcce5f0 100644 --- a/deployment.go +++ b/deployment.go @@ -1,7 +1,77 @@ package switchblade +import ( + "context" + "fmt" + "io" + + "github.com/cloudfoundry/switchblade/internal/cloudfoundry" + "github.com/docker/docker/api/types/container" +) + +//go:generate faux --interface LogsClient --output fakes/logs_client.go +type LogsClient interface { + ContainerLogs(ctx context.Context, container string, options container.LogsOptions) (io.ReadCloser, error) +} + type Deployment struct { Name string ExternalURL string InternalURL string + + // Internal fields for log retrieval + platform string + workspace string + cfCLI cloudfoundry.Executable + dockerCLI LogsClient +} + +// RuntimeLogs retrieves recent logs from the running application. +// These are logs generated after the application has started (post-staging). +// This method abstracts platform-specific log retrieval for both +// CloudFoundry and Docker platforms. +// +// Use this for testing: +// - Application startup messages +// - Service connections +// - Module/extension loading +// - Runtime configuration +// +// For build-time logs (staging, buildpack detection), use the logs +// returned from platform.Deploy.Execute() instead. +func (d Deployment) RuntimeLogs() (string, error) { + switch d.platform { + case CloudFoundry: + return d.logsCloudFoundry() + case Docker: + return d.logsDocker() + default: + return "", fmt.Errorf("unknown platform type: %q", d.platform) + } +} + +func (d Deployment) logsCloudFoundry() (string, error) { + return cloudfoundry.FetchRecentLogs(d.cfCLI, d.workspace, d.Name) +} + +func (d Deployment) logsDocker() (string, error) { + ctx := context.Background() + + options := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + } + + reader, err := d.dockerCLI.ContainerLogs(ctx, d.Name, options) + if err != nil { + return "", fmt.Errorf("failed to retrieve container logs: %w", err) + } + defer reader.Close() + + logs, err := io.ReadAll(reader) + if err != nil { + return "", fmt.Errorf("failed to read logs: %w", err) + } + + return string(logs), nil } diff --git a/docker.go b/docker.go index 16d4406..9042a28 100644 --- a/docker.go +++ b/docker.go @@ -15,11 +15,11 @@ import ( //go:generate faux --package github.com/cloudfoundry/switchblade/internal/docker --interface StartPhase --name DockerStartPhase --output fakes/docker_start_phase.go //go:generate faux --package github.com/cloudfoundry/switchblade/internal/docker --interface TeardownPhase --name DockerTeardownPhase --output fakes/docker_teardown_phase.go -func NewDocker(initialize docker.InitializePhase, deinitialize docker.DeinitializePhase, setup docker.SetupPhase, stage docker.StagePhase, start docker.StartPhase, teardown docker.TeardownPhase) Platform { +func NewDocker(initialize docker.InitializePhase, deinitialize docker.DeinitializePhase, setup docker.SetupPhase, stage docker.StagePhase, start docker.StartPhase, teardown docker.TeardownPhase, client LogsClient) Platform { return Platform{ initialize: dockerInitializeProcess{initialize: initialize}, deinitialize: dockerDeinitializeProcess{deinitialize: deinitialize}, - Deploy: dockerDeployProcess{setup: setup, stage: stage, start: start}, + Deploy: dockerDeployProcess{setup: setup, stage: stage, start: start, client: client}, Delete: dockerDeleteProcess{teardown: teardown}, } } @@ -49,9 +49,10 @@ func (p dockerDeinitializeProcess) Execute() error { } type dockerDeployProcess struct { - setup docker.SetupPhase - stage docker.StagePhase - start docker.StartPhase + setup docker.SetupPhase + stage docker.StagePhase + start docker.StartPhase + client LogsClient } func (p dockerDeployProcess) WithBuildpacks(buildpacks ...string) DeployProcess { @@ -120,6 +121,8 @@ func (p dockerDeployProcess) Execute(name, path string) (Deployment, fmt.Stringe Name: name, ExternalURL: externalURL, InternalURL: internalURL, + platform: Docker, + dockerCLI: p.client, }, logs, nil } diff --git a/docker_test.go b/docker_test.go index da3c4ca..2c3416a 100644 --- a/docker_test.go +++ b/docker_test.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "io" + "strings" "testing" "github.com/cloudfoundry/switchblade" "github.com/cloudfoundry/switchblade/fakes" "github.com/cloudfoundry/switchblade/internal/docker" + "github.com/docker/docker/api/types/container" "github.com/sclevine/spec" . "github.com/cloudfoundry/switchblade/matchers" @@ -28,6 +30,7 @@ func testDocker(t *testing.T, context spec.G, it spec.S) { stage *fakes.DockerStagePhase start *fakes.DockerStartPhase teardown *fakes.DockerTeardownPhase + client *fakes.LogsClient ) it.Before(func() { @@ -37,8 +40,9 @@ func testDocker(t *testing.T, context spec.G, it spec.S) { stage = &fakes.DockerStagePhase{} start = &fakes.DockerStartPhase{} teardown = &fakes.DockerTeardownPhase{} + client = &fakes.LogsClient{} - platform = switchblade.NewDocker(initialize, deinitialize, setup, stage, start, teardown) + platform = switchblade.NewDocker(initialize, deinitialize, setup, stage, start, teardown, client) }) context("Initialize", func() { @@ -139,11 +143,9 @@ func testDocker(t *testing.T, context spec.G, it spec.S) { "Staging...", "Starting...", )) - Expect(deployment).To(Equal(switchblade.Deployment{ - Name: "some-app", - ExternalURL: "some-external-url", - InternalURL: "some-internal-url", - })) + Expect(deployment.Name).To(Equal("some-app")) + Expect(deployment.ExternalURL).To(Equal("some-external-url")) + Expect(deployment.InternalURL).To(Equal("some-internal-url")) Expect(setup.RunCall.Receives.Ctx).To(Equal(gocontext.Background())) Expect(setup.RunCall.Receives.Logs).To(Equal(logs)) @@ -161,6 +163,37 @@ func testDocker(t *testing.T, context spec.G, it spec.S) { Expect(start.RunCall.Receives.Command).To(Equal("some-command")) }) + it("retrieves runtime logs from deployed application", func() { + client.ContainerLogsCall.Stub = func(ctx gocontext.Context, container string, options container.LogsOptions) (io.ReadCloser, error) { + return io.NopCloser(io.NewSectionReader( + strings.NewReader("Docker runtime log 1\nApplication is running\nDocker runtime log 2\n"), + 0, + 100, + )), nil + } + + deployment, stagingLogs, err := platform.Deploy.Execute("some-app", "/some/path/to/my/app") + Expect(err).NotTo(HaveOccurred()) + + // Staging logs should contain setup/build output + Expect(stagingLogs.String()).To(ContainSubstring("Setting up...")) + Expect(stagingLogs.String()).To(ContainSubstring("Staging...")) + Expect(stagingLogs.String()).To(ContainSubstring("Starting...")) + + // Runtime logs should contain application output + runtimeLogs, err := deployment.RuntimeLogs() + Expect(err).NotTo(HaveOccurred()) + Expect(runtimeLogs).To(ContainSubstring("Docker runtime log 1")) + Expect(runtimeLogs).To(ContainSubstring("Application is running")) + Expect(runtimeLogs).To(ContainSubstring("Docker runtime log 2")) + + // Verify the client was called with correct parameters + Expect(client.ContainerLogsCall.CallCount).To(Equal(1)) + Expect(client.ContainerLogsCall.Receives.Container).To(Equal("some-app")) + Expect(client.ContainerLogsCall.Receives.Options.ShowStdout).To(BeTrue()) + Expect(client.ContainerLogsCall.Receives.Options.ShowStderr).To(BeTrue()) + }) + context("WithBuildpacks", func() { it("uses those buildpacks", func() { platform.Deploy.WithBuildpacks("some-buildpack", "other-buildpack") diff --git a/fakes/logs_client.go b/fakes/logs_client.go new file mode 100644 index 0000000..19bae8a --- /dev/null +++ b/fakes/logs_client.go @@ -0,0 +1,39 @@ +package fakes + +import ( + "context" + "io" + "sync" + + "github.com/docker/docker/api/types/container" +) + +type LogsClient struct { + ContainerLogsCall struct { + mutex sync.Mutex + CallCount int + Receives struct { + Ctx context.Context + Container string + Options container.LogsOptions + } + Returns struct { + ReadCloser io.ReadCloser + Error error + } + Stub func(context.Context, string, container.LogsOptions) (io.ReadCloser, error) + } +} + +func (f *LogsClient) ContainerLogs(ctx context.Context, containerName string, options container.LogsOptions) (io.ReadCloser, error) { + f.ContainerLogsCall.mutex.Lock() + defer f.ContainerLogsCall.mutex.Unlock() + f.ContainerLogsCall.CallCount++ + f.ContainerLogsCall.Receives.Ctx = ctx + f.ContainerLogsCall.Receives.Container = containerName + f.ContainerLogsCall.Receives.Options = options + if f.ContainerLogsCall.Stub != nil { + return f.ContainerLogsCall.Stub(ctx, containerName, options) + } + return f.ContainerLogsCall.Returns.ReadCloser, f.ContainerLogsCall.Returns.Error +} diff --git a/internal/cloudfoundry/init_test.go b/internal/cloudfoundry/init_test.go index 5d5607e..1c42d5d 100644 --- a/internal/cloudfoundry/init_test.go +++ b/internal/cloudfoundry/init_test.go @@ -13,6 +13,7 @@ func TestCloudFoundry(t *testing.T) { suite := spec.New("switchblade/internal/cloudfoundry", spec.Report(report.Terminal{}), spec.Parallel()) suite("Initialize", testInitialize) + suite("Logs", testLogs) suite("Setup", testSetup) suite("Stage", testStage) suite("Teardown", testTeardown) diff --git a/internal/cloudfoundry/logs.go b/internal/cloudfoundry/logs.go new file mode 100644 index 0000000..2c445d7 --- /dev/null +++ b/internal/cloudfoundry/logs.go @@ -0,0 +1,28 @@ +package cloudfoundry + +import ( + "bytes" + "fmt" + "os" + + "github.com/paketo-buildpacks/packit/v2/pexec" +) + +// FetchRecentLogs retrieves recent application logs using 'cf logs --recent'. +// This is a shared helper used for both staging failures and runtime log retrieval. +func FetchRecentLogs(cli Executable, home, appName string) (string, error) { + env := append(os.Environ(), fmt.Sprintf("CF_HOME=%s", home)) + buffer := bytes.NewBuffer(nil) + + err := cli.Execute(pexec.Execution{ + Args: []string{"logs", appName, "--recent"}, + Stdout: buffer, + Stderr: buffer, + Env: env, + }) + if err != nil { + return "", fmt.Errorf("failed to retrieve logs: %w", err) + } + + return buffer.String(), nil +} diff --git a/internal/cloudfoundry/logs_test.go b/internal/cloudfoundry/logs_test.go new file mode 100644 index 0000000..2d1c14a --- /dev/null +++ b/internal/cloudfoundry/logs_test.go @@ -0,0 +1,60 @@ +package cloudfoundry_test + +import ( + "fmt" + "testing" + + "github.com/cloudfoundry/switchblade/internal/cloudfoundry" + "github.com/cloudfoundry/switchblade/internal/cloudfoundry/fakes" + "github.com/paketo-buildpacks/packit/v2/pexec" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testLogs(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + cli *fakes.Executable + ) + + it.Before(func() { + cli = &fakes.Executable{} + }) + + context("FetchRecentLogs", func() { + it("fetches recent logs using cf logs --recent", func() { + cli.ExecuteCall.Stub = func(execution pexec.Execution) error { + if len(execution.Args) > 0 && execution.Args[0] == "logs" { + fmt.Fprintln(execution.Stdout, "Log line 1") + fmt.Fprintln(execution.Stdout, "Log line 2") + fmt.Fprintln(execution.Stdout, "Log line 3") + } + return nil + } + + logs, err := cloudfoundry.FetchRecentLogs(cli, "/tmp/some-home", "some-app") + Expect(err).NotTo(HaveOccurred()) + Expect(logs).To(ContainSubstring("Log line 1")) + Expect(logs).To(ContainSubstring("Log line 2")) + Expect(logs).To(ContainSubstring("Log line 3")) + + Expect(cli.ExecuteCall.CallCount).To(Equal(1)) + Expect(cli.ExecuteCall.Receives.Execution.Args).To(Equal([]string{"logs", "some-app", "--recent"})) + Expect(cli.ExecuteCall.Receives.Execution.Env).To(ContainElement(ContainSubstring("CF_HOME=/tmp/some-home"))) + }) + + context("when cf logs command fails", func() { + it.Before(func() { + cli.ExecuteCall.Returns.Error = fmt.Errorf("cf logs failed") + }) + + it("returns an error", func() { + _, err := cloudfoundry.FetchRecentLogs(cli, "/tmp/some-home", "some-app") + Expect(err).To(MatchError(ContainSubstring("failed to retrieve logs"))) + Expect(err).To(MatchError(ContainSubstring("cf logs failed"))) + }) + }) + }) +} diff --git a/internal/cloudfoundry/stage.go b/internal/cloudfoundry/stage.go index cee76d1..dcefd06 100644 --- a/internal/cloudfoundry/stage.go +++ b/internal/cloudfoundry/stage.go @@ -37,17 +37,11 @@ func (s Stage) Run(logs io.Writer, home, name string) (string, error) { if err != nil { // In CF API v3, staging failure logs are not automatically captured in stdout/stderr // We need to fetch them explicitly using 'cf logs --recent' - recentLogs := bytes.NewBuffer(nil) - logErr := s.cli.Execute(pexec.Execution{ - Args: []string{"logs", name, "--recent"}, - Stdout: recentLogs, - Stderr: recentLogs, - Env: env, - }) - if logErr == nil && recentLogs.Len() > 0 { + recentLogs, logErr := FetchRecentLogs(s.cli, home, name) + if logErr == nil && len(recentLogs) > 0 { // Append recent logs to the main logs buffer _, _ = logs.Write([]byte("\n--- Recent Logs (cf logs --recent) ---\n")) - _, _ = logs.Write(recentLogs.Bytes()) + _, _ = logs.Write([]byte(recentLogs)) } return "", fmt.Errorf("failed to start: %w\n\nOutput:\n%s", err, logs) diff --git a/platform.go b/platform.go index fc232e8..20a7923 100644 --- a/platform.go +++ b/platform.go @@ -71,9 +71,9 @@ func NewPlatform(platformType, token, stack string) (Platform, error) { stage := cloudfoundry.NewStage(cli) teardown := cloudfoundry.NewTeardown(cli) - return NewCloudFoundry(initialize, deinitialize, setup, stage, teardown, os.TempDir()), nil + return NewCloudFoundry(initialize, deinitialize, setup, stage, teardown, os.TempDir(), cli), nil case Docker: - client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return Platform{}, err } @@ -86,16 +86,16 @@ func NewPlatform(platformType, token, stack string) (Platform, error) { buildpacksCache := docker.NewBuildpacksCache(filepath.Join(workspace, "buildpacks-cache")) buildpacksRegistry := docker.NewBuildpacksRegistry("https://api.github.com", token) buildpacksManager := docker.NewBuildpacksManager(archiver, buildpacksCache, buildpacksRegistry) - networkManager := docker.NewNetworkManager(client) + networkManager := docker.NewNetworkManager(dockerClient) initialize := docker.NewInitialize(buildpacksRegistry, networkManager) deinitialize := docker.NewDeinitialize(networkManager) - setup := docker.NewSetup(client, lifecycleManager, buildpacksManager, archiver, networkManager, workspace, stack) - stage := docker.NewStage(client, archiver, workspace) - start := docker.NewStart(client, networkManager, workspace, stack) - teardown := docker.NewTeardown(client, workspace) + setup := docker.NewSetup(dockerClient, lifecycleManager, buildpacksManager, archiver, networkManager, workspace, stack) + stage := docker.NewStage(dockerClient, archiver, workspace) + start := docker.NewStart(dockerClient, networkManager, workspace, stack) + teardown := docker.NewTeardown(dockerClient, workspace) - return NewDocker(initialize, deinitialize, setup, stage, start, teardown), nil + return NewDocker(initialize, deinitialize, setup, stage, start, teardown, dockerClient), nil } return Platform{}, fmt.Errorf("unknown platform type: %q", platformType)