Skip to content
Draft
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
27 changes: 27 additions & 0 deletions pkg/apiclient/client_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/MakeNowJust/heredoc/v2"
"github.com/OctopusDeploy/cli/pkg/constants"
"github.com/OctopusDeploy/cli/pkg/dryrun"
"github.com/OctopusDeploy/cli/pkg/output"
"github.com/OctopusDeploy/cli/pkg/question"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
Expand Down Expand Up @@ -45,6 +46,12 @@ type ClientFactory interface {

// GetHttpClient returns a raw http client which can be used to query Octopus
GetHttpClient() (*http.Client, error)

// SetDryRun puts the client into dry-run mode, where any request that would change
// server state is refused before it is sent. It backstops the per-command --dry-run
// implementations; a command which hasn't finished implementing dry run fails loudly
// rather than mutating Octopus while claiming it did not.
SetDryRun(enabled bool)
}

type Client struct {
Expand Down Expand Up @@ -73,6 +80,9 @@ type Client struct {
ActiveSpace *spaces.Space

Ask question.AskProvider

// true once the dry-run guard has been installed on HttpClient
dryRun bool
}

func NewClientFactory(httpClient *http.Client, host string, credentials octopusApiClient.ICredential, spaceNameOrID string, ask question.AskProvider) (ClientFactory, error) {
Expand Down Expand Up @@ -257,6 +267,21 @@ func (c *Client) GetHttpClient() (*http.Client, error) {
return c.HttpClient, nil
}

// SetDryRun wraps the transport in the dry-run guard. It must be called before the
// space-scoped or system clients are created, which is why the root command arms it
// from PersistentPreRun; both clients are built lazily during RunE.
func (c *Client) SetDryRun(enabled bool) {
if !enabled || c.dryRun {
return
}
c.dryRun = true

if c.HttpClient == nil {
c.HttpClient = &http.Client{}
}
c.HttpClient.Transport = dryrun.NewGuardRoundTripper(c.HttpClient.Transport)
}

func (c *Client) SetSpaceNameOrId(spaceNameOrId string) {
// technically don't need to nil out the SystemClient, but it's cleaner that way
// because a SpaceScopedClient can also be a SystemClient
Expand Down Expand Up @@ -408,3 +433,5 @@ func (s *stubClientFactory) GetHostUrl() string { return "" }
func (s *stubClientFactory) GetHttpClient() (*http.Client, error) {
return nil, nil
}

func (s *stubClientFactory) SetDryRun(_ bool) {}
32 changes: 32 additions & 0 deletions pkg/apiclient/client_factory_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package apiclient_test

import (
"bytes"
"io"
"net/http"
"testing"

"github.com/OctopusDeploy/cli/pkg/apiclient"
Expand Down Expand Up @@ -67,3 +70,32 @@ func TestNewClientFactory_WhenHostAndAccessTokenAreSupplied_ReturnsClientFactory
testutil.RequireSuccess(t, err)
assert.NotNil(t, factory)
}

type recordingRoundTripper struct {
Requests []*http.Request
}

func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
r.Requests = append(r.Requests, req)
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(nil))}, nil
}

func TestClientFactory_SetDryRun_RefusesMutatingRequests(t *testing.T) {
transport := &recordingRoundTripper{}
apiKeyCredential, _ := client.NewApiKey(apiKey)
clientFactory, err := apiclient.NewClientFactory(&http.Client{Transport: transport}, hostUrl, apiKeyCredential, "", qa)
testutil.RequireSuccess(t, err)

clientFactory.SetDryRun(true)

httpClient, err := clientFactory.GetHttpClient()
testutil.RequireSuccess(t, err)

_, err = httpClient.Post(hostUrl+"/api/Spaces-1/releases/create/v1", "application/json", nil)
assert.ErrorContains(t, err, "dry run blocked a POST request to /api/Spaces-1/releases/create/v1")
assert.Empty(t, transport.Requests, "a mutating request must not reach the server")

_, err = httpClient.Get(hostUrl + "/api/Spaces-1/projects/all")
assert.Nil(t, err)
assert.Len(t, transport.Requests, 1, "read-only requests still go through")
}
Loading