From 0fa97d877e971a3bf66a3b8f1524d168e1a0de34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Thu, 15 Oct 2026 16:47:11 +0200 Subject: [PATCH 1/5] refactor(kosli): add GetEnvironment --- internal/kosli/client_test.go | 6 +++++- internal/kosli/environments.go | 27 ++++++++++++++++++++++++ internal/kosli/environments_test.go | 32 +++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 internal/kosli/environments.go create mode 100644 internal/kosli/environments_test.go diff --git a/internal/kosli/client_test.go b/internal/kosli/client_test.go index 5a32ae7b9..9ddf33771 100644 --- a/internal/kosli/client_test.go +++ b/internal/kosli/client_test.go @@ -21,6 +21,7 @@ type received struct { method string rawPath string authHeader string + body string } func newFakeServer(t *testing.T, status int, responseBody string) (*httptest.Server, *received) { @@ -31,8 +32,11 @@ func newFakeServer(t *testing.T, status int, responseBody string) (*httptest.Ser seen.method = r.Method seen.rawPath = r.URL.EscapedPath() seen.authHeader = r.Header.Get("Authorization") + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + seen.body = string(body) w.WriteHeader(status) - _, err := w.Write([]byte(responseBody)) + _, err = w.Write([]byte(responseBody)) // assert, not require: FailNow must not be called off the test goroutine. assert.NoError(t, err) })) diff --git a/internal/kosli/environments.go b/internal/kosli/environments.go new file mode 100644 index 000000000..0d5007193 --- /dev/null +++ b/internal/kosli/environments.go @@ -0,0 +1,27 @@ +package kosli + +import ( + "context" + "net/http" + + "github.com/kosli-dev/cli/internal/requests" +) + +// Environment is an Environment's metadata, with the fields callers read so far. +type Environment struct { + Name string `json:"name"` + Type string `json:"type"` +} + +// GetEnvironment calls get_environment. It returns the Environment called name. +func (c *Client) GetEnvironment(ctx context.Context, name string) (*Response[Environment], error) { + endpoint, err := c.endpoint("environments", c.org, name) + if err != nil { + return nil, err + } + result, err := c.send(ctx, &requests.RequestParams{Method: http.MethodGet, URL: endpoint}) + if err != nil { + return nil, err + } + return &Response[Environment]{Result: *result}, nil +} diff --git a/internal/kosli/environments_test.go b/internal/kosli/environments_test.go new file mode 100644 index 000000000..9de73d159 --- /dev/null +++ b/internal/kosli/environments_test.go @@ -0,0 +1,32 @@ +package kosli + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetEnvironmentRequestsTheEnvironmentAndDecodesItsType(t *testing.T) { + server, seen := newFakeServer(t, http.StatusOK, `{"name":"prod","type":"K8S","last_reported_at":null}`) + client := newTestClient(t, server.URL) + + environment, err := client.GetEnvironment(context.Background(), "prod") + + require.NoError(t, err) + require.Equal(t, http.MethodGet, seen.method) + require.Equal(t, "/api/v2/environments/test-org/prod", seen.rawPath) + value, err := environment.Value() + require.NoError(t, err) + require.Equal(t, Environment{Name: "prod", Type: "K8S"}, value) +} + +func TestGetEnvironmentOfAMissingEnvironmentIsNotFound(t *testing.T) { + server, _ := newFakeServer(t, http.StatusNotFound, `{"message":"Environment not found"}`) + client := newTestClient(t, server.URL) + + _, err := client.GetEnvironment(context.Background(), "missing") + + require.True(t, IsNotFound(err)) +} From 9804b18bcd435dd479f53bc4097663491d5ff105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Thu, 15 Oct 2026 16:47:28 +0200 Subject: [PATCH 2/5] refactor(kosli): add CreateEnvironment and ReportSnapshot --- internal/kosli/environments.go | 31 ++++++++++++++++++++++ internal/kosli/environments_test.go | 40 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/internal/kosli/environments.go b/internal/kosli/environments.go index 0d5007193..88b8613cf 100644 --- a/internal/kosli/environments.go +++ b/internal/kosli/environments.go @@ -25,3 +25,34 @@ func (c *Client) GetEnvironment(ctx context.Context, name string) (*Response[Env } return &Response[Environment]{Result: *result}, nil } + +// CreateEnvironmentRequest describes the Environment CreateEnvironment creates. +type CreateEnvironmentRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + IncludeScaling *bool `json:"include_scaling,omitempty"` + RequireProvenance *bool `json:"require_provenance,omitempty"` + IncludedEnvironments []string `json:"included_environments,omitempty"` +} + +// CreateEnvironment calls create_or_update_environment. It creates the +// Environment, or updates it when one with that name already exists. +func (c *Client) CreateEnvironment(ctx context.Context, request CreateEnvironmentRequest) (*Result, error) { + endpoint, err := c.endpoint("environments", c.org) + if err != nil { + return nil, err + } + return c.send(ctx, &requests.RequestParams{Method: http.MethodPut, URL: endpoint, Payload: request}) +} + +// ReportSnapshot calls the report operation for envType, such as +// report_k8s_environment or report_ecs_environment. It reports payload as +// the current Snapshot of the Environment called name. +func (c *Client) ReportSnapshot(ctx context.Context, name, envType string, payload any) (*Result, error) { + endpoint, err := c.endpoint("environments", c.org, name, "report", envType) + if err != nil { + return nil, err + } + return c.send(ctx, &requests.RequestParams{Method: http.MethodPut, URL: endpoint, Payload: payload}) +} diff --git a/internal/kosli/environments_test.go b/internal/kosli/environments_test.go index 9de73d159..4dbe7e80a 100644 --- a/internal/kosli/environments_test.go +++ b/internal/kosli/environments_test.go @@ -30,3 +30,43 @@ func TestGetEnvironmentOfAMissingEnvironmentIsNotFound(t *testing.T) { require.True(t, IsNotFound(err)) } + +func TestCreateEnvironmentPutsTheRequestToTheOrgsEnvironments(t *testing.T) { + server, seen := newFakeServer(t, http.StatusCreated, ``) + client := newTestClient(t, server.URL) + include := true + + result, err := client.CreateEnvironment(context.Background(), CreateEnvironmentRequest{ + Name: "prod", Type: "K8S", Description: "production", IncludeScaling: &include, + }) + + require.NoError(t, err) + require.True(t, result.Created) + require.Equal(t, http.MethodPut, seen.method) + require.Equal(t, "/api/v2/environments/test-org", seen.rawPath) + require.JSONEq(t, `{"name":"prod","type":"K8S","description":"production","include_scaling":true}`, seen.body) +} + +func TestReportSnapshotPutsThePayloadToTheEnvironmentTypesReport(t *testing.T) { + server, seen := newFakeServer(t, http.StatusCreated, ``) + client := newTestClient(t, server.URL) + + _, err := client.ReportSnapshot(context.Background(), "prod eu", "azure-apps", map[string]any{"artifacts": []any{}}) + + require.NoError(t, err) + require.Equal(t, http.MethodPut, seen.method) + require.Equal(t, "/api/v2/environments/test-org/prod%20eu/report/azure-apps", seen.rawPath) + require.JSONEq(t, `{"artifacts":[]}`, seen.body) +} + +func TestADryRunSendsNeitherEnvironmentWrite(t *testing.T) { + server, seen := newFakeServer(t, http.StatusCreated, ``) + client := New(DryRun(newHTTPClient(t)), server.URL, "test-org", "test-token") + + _, err := client.CreateEnvironment(context.Background(), CreateEnvironmentRequest{Name: "prod", Type: "K8S"}) + require.NoError(t, err) + _, err = client.ReportSnapshot(context.Background(), "prod", "K8S", map[string]any{}) + require.NoError(t, err) + + require.Equal(t, 0, seen.hits) +} From c775f66803f5ad6546fc0af3a8922bc4b628c36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Thu, 15 Oct 2026 16:49:00 +0200 Subject: [PATCH 3/5] refactor(get environment, create environment): go through the Kosli API client --- cmd/kosli/createEnvironment.go | 33 +++++------------------ cmd/kosli/getEnvironment.go | 22 ++++----------- cmd/kosli/openapiContract_test.go | 3 ++- cmd/kosli/snapshotAutoEnvironment.go | 3 ++- cmd/kosli/snapshotAutoEnvironment_test.go | 6 +++-- cmd/kosli/testHelpers.go | 6 +++-- 6 files changed, 23 insertions(+), 50 deletions(-) diff --git a/cmd/kosli/createEnvironment.go b/cmd/kosli/createEnvironment.go index a19bfbce3..5de34c717 100644 --- a/cmd/kosli/createEnvironment.go +++ b/cmd/kosli/createEnvironment.go @@ -1,11 +1,10 @@ package main import ( + "context" "io" - "net/http" - "net/url" - "github.com/kosli-dev/cli/internal/requests" + "github.com/kosli-dev/cli/internal/kosli" "github.com/spf13/cobra" ) @@ -50,21 +49,12 @@ kosli create environment yourLogicalEnvironmentName ` type createEnvOptions struct { - payload CreateEnvironmentPayload + payload kosli.CreateEnvironmentRequest excludeScaling bool includeScaling bool requireProvenance bool } -type CreateEnvironmentPayload struct { - Name string `json:"name"` - Type string `json:"type"` - Description string `json:"description"` - IncludeScaling *bool `json:"include_scaling,omitempty"` - RequireProvenance *bool `json:"require_provenance,omitempty"` - IncludedEnvironments []string `json:"included_environments,omitempty"` -} - func newCreateEnvironmentCmd(out io.Writer) *cobra.Command { o := new(createEnvOptions) cmd := &cobra.Command{ @@ -86,7 +76,7 @@ func newCreateEnvironmentCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -117,12 +107,8 @@ func newCreateEnvironmentCmd(out io.Writer) *cobra.Command { return cmd } -func (o *createEnvOptions) run(args []string) error { +func (o *createEnvOptions) run(ctx context.Context, args []string) error { o.payload.Name = args[0] - url, err := url.JoinPath(global.Host, "api/v2/environments", global.Org) - if err != nil { - return err - } if o.includeScaling { var myTrue = true @@ -137,14 +123,7 @@ func (o *createEnvOptions) run(args []string) error { o.payload.RequireProvenance = &myTrue } - reqParams := &requests.RequestParams{ - Method: http.MethodPut, - URL: url, - Payload: o.payload, - DryRun: global.DryRun, - Token: global.ApiToken, - } - _, err = kosliClient.Do(reqParams) + _, err := kosliAPI().CreateEnvironment(ctx, o.payload) if err == nil && !global.DryRun { logger.Info("environment %s was created", o.payload.Name) } diff --git a/cmd/kosli/getEnvironment.go b/cmd/kosli/getEnvironment.go index ef37d0686..827b7851a 100644 --- a/cmd/kosli/getEnvironment.go +++ b/cmd/kosli/getEnvironment.go @@ -1,14 +1,12 @@ package main import ( + "context" "encoding/json" "fmt" "io" - "net/http" - "net/url" "github.com/kosli-dev/cli/internal/output" - "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" ) @@ -34,7 +32,7 @@ func newGetEnvironmentCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(out, args) + return o.run(cmd.Context(), out, args) }, } @@ -43,23 +41,13 @@ func newGetEnvironmentCmd(out io.Writer) *cobra.Command { return cmd } -func (o *getEnvironmentOptions) run(out io.Writer, args []string) error { - url, err := url.JoinPath(global.Host, "api/v2/environments", global.Org, args[0]) +func (o *getEnvironmentOptions) run(ctx context.Context, out io.Writer, args []string) error { + environment, err := kosliAPI().GetEnvironment(ctx, args[0]) if err != nil { return err } - reqParams := &requests.RequestParams{ - Method: http.MethodGet, - URL: url, - Token: global.ApiToken, - } - response, err := kosliClient.Do(reqParams) - if err != nil { - return err - } - - return output.FormattedPrint(response.Body, o.output, out, 0, + return output.FormattedPrint(string(environment.Raw), o.output, out, 0, map[string]output.FormatOutputFunc{ "table": printEnvironmentAsTable, "json": output.PrintJson, diff --git a/cmd/kosli/openapiContract_test.go b/cmd/kosli/openapiContract_test.go index fc4a272d9..6d46ee20a 100644 --- a/cmd/kosli/openapiContract_test.go +++ b/cmd/kosli/openapiContract_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/kosli-dev/cli/internal/kosli" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -66,7 +67,7 @@ func (suite *OpenAPIContractTestSuite) SetupSuite() { func (suite *OpenAPIContractTestSuite) TestPayloadsMatchSchema() { registry := []driftCase{ {name: "create control", payload: ControlPayload{}, component: "ControlPostInput"}, - {name: "create environment", payload: CreateEnvironmentPayload{}, component: "CreateEnvironmentPutInput"}, + {name: "create environment", payload: kosli.CreateEnvironmentRequest{}, component: "CreateEnvironmentPutInput"}, } for _, c := range registry { diff --git a/cmd/kosli/snapshotAutoEnvironment.go b/cmd/kosli/snapshotAutoEnvironment.go index 59dafd5e4..105be129b 100644 --- a/cmd/kosli/snapshotAutoEnvironment.go +++ b/cmd/kosli/snapshotAutoEnvironment.go @@ -8,6 +8,7 @@ import ( "net/url" "strings" + "github.com/kosli-dev/cli/internal/kosli" "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -156,7 +157,7 @@ func createEnvironmentForSnapshot(envName, envType string) error { description = defaultAutoEnvDescription } - payload := CreateEnvironmentPayload{ + payload := kosli.CreateEnvironmentRequest{ Name: envName, Type: envType, Description: description, diff --git a/cmd/kosli/snapshotAutoEnvironment_test.go b/cmd/kosli/snapshotAutoEnvironment_test.go index 248e3a00a..edc4b5d73 100644 --- a/cmd/kosli/snapshotAutoEnvironment_test.go +++ b/cmd/kosli/snapshotAutoEnvironment_test.go @@ -1,10 +1,12 @@ package main import ( + "context" "fmt" "os" "testing" + "github.com/kosli-dev/cli/internal/kosli" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -53,14 +55,14 @@ func (suite *SnapshotAutoEnvironmentTestSuite) SetupSuite() { func (suite *SnapshotAutoEnvironmentTestSuite) createLogicalEnv(name string, included []string) { suite.T().Helper() o := &createEnvOptions{ - payload: CreateEnvironmentPayload{ + payload: kosli.CreateEnvironmentRequest{ Name: name, Type: "logical", Description: "test logical env", IncludedEnvironments: included, }, } - require.NoError(suite.T(), o.run([]string{name}), "logical env should be created without error") + require.NoError(suite.T(), o.run(context.Background(), []string{name}), "logical env should be created without error") } func (suite *SnapshotAutoEnvironmentTestSuite) TestSnapshotAutoEnvironment() { diff --git a/cmd/kosli/testHelpers.go b/cmd/kosli/testHelpers.go index a92a0e89b..f3bbd1d4c 100644 --- a/cmd/kosli/testHelpers.go +++ b/cmd/kosli/testHelpers.go @@ -3,6 +3,7 @@ package main import ( "bufio" "bytes" + "context" "encoding/json" "io" "net/http" @@ -15,6 +16,7 @@ import ( "testing" "github.com/kosli-dev/cli/internal/gitview" + "github.com/kosli-dev/cli/internal/kosli" "github.com/kosli-dev/cli/internal/requests" shellwords "github.com/mattn/go-shellwords" "github.com/pkg/errors" @@ -489,14 +491,14 @@ func EnableBeta(t *testing.T) { func CreateEnv(org, envName, envType string, t *testing.T) { t.Helper() o := &createEnvOptions{ - payload: CreateEnvironmentPayload{ + payload: kosli.CreateEnvironmentRequest{ Name: envName, Type: envType, Description: "test env", }, } - err := o.run([]string{envName}) + err := o.run(context.Background(), []string{envName}) require.NoError(t, err, "env should be created without error") } From b0b79bd8f35a5b14d76fc7231e1d6477431998b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Thu, 15 Oct 2026 16:51:56 +0200 Subject: [PATCH 4/5] refactor(snapshot): report snapshots and auto-create environments through the Kosli API client --- cmd/kosli/snapshotAutoEnvironment.go | 51 +++++++--------------------- cmd/kosli/snapshotAzureApps.go | 7 ++-- cmd/kosli/snapshotCloudRun.go | 6 ++-- cmd/kosli/snapshotDocker.go | 6 ++-- cmd/kosli/snapshotECS.go | 7 ++-- cmd/kosli/snapshotGKE.go | 6 ++-- cmd/kosli/snapshotK8S.go | 17 +++++----- cmd/kosli/snapshotLambda.go | 7 ++-- cmd/kosli/snapshotPath.go | 17 +++++----- cmd/kosli/snapshotPaths.go | 9 ++--- cmd/kosli/snapshotReport.go | 39 ++++++--------------- cmd/kosli/snapshotReport_test.go | 47 +++++++++++++------------ cmd/kosli/snapshotS3.go | 7 ++-- cmd/kosli/snapshotServer.go | 7 ++-- cmd/kosli/testHelpers.go | 2 +- 15 files changed, 101 insertions(+), 134 deletions(-) diff --git a/cmd/kosli/snapshotAutoEnvironment.go b/cmd/kosli/snapshotAutoEnvironment.go index 105be129b..254d802fa 100644 --- a/cmd/kosli/snapshotAutoEnvironment.go +++ b/cmd/kosli/snapshotAutoEnvironment.go @@ -1,15 +1,11 @@ package main import ( - "encoding/json" - "errors" + "context" "fmt" - "net/http" - "net/url" "strings" "github.com/kosli-dev/cli/internal/kosli" - "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -61,7 +57,7 @@ func addAutoEnvironmentFlags(cmd *cobra.Command) { // reported, when --auto-environment is set. It is a no-op when the flag is not // set or when the environment already exists with a matching type. envType is // inferred from the snapshot subcommand (e.g. "docker", "K8S"). -func ensureEnvironment(envName, envType string) error { +func ensureEnvironment(ctx context.Context, envName, envType string) error { o := snapshotAutoEnv // Enforce the scaling-flag mutual exclusion unconditionally, mirroring @@ -85,7 +81,7 @@ func ensureEnvironment(envName, envType string) error { return nil } - exists, existingType, err := getEnvironmentTypeIfExists(envName) + exists, existingType, err := getEnvironmentTypeIfExists(ctx, envName) if err != nil { return err } @@ -102,7 +98,7 @@ func ensureEnvironment(envName, envType string) error { return nil } - return createEnvironmentForSnapshot(envName, envType) + return createEnvironmentForSnapshot(ctx, envName, envType) } // getEnvironmentTypeIfExists fetches the environment's metadata and returns @@ -110,48 +106,31 @@ func ensureEnvironment(envName, envType string) error { // "does not exist" (exists=false, nil error); any other error (auth, network, // 5xx after retries) is returned to the caller so it is never mistaken for a // missing environment. -func getEnvironmentTypeIfExists(envName string) (bool, string, error) { - reqURL, err := url.JoinPath(global.Host, "api/v2/environments", global.Org, envName) - if err != nil { - return false, "", err - } - - reqParams := &requests.RequestParams{ - Method: http.MethodGet, - URL: reqURL, - Token: global.ApiToken, - } - response, err := kosliClient.Do(reqParams) +func getEnvironmentTypeIfExists(ctx context.Context, envName string) (bool, string, error) { + response, err := kosliAPI().GetEnvironment(ctx, envName) if err != nil { // Only a genuine 404 means the environment does not exist yet. Any other // error (auth, network, 5xx after retries) is propagated so we never fall // through to a create-or-update PUT that could overwrite an existing // environment or bypass the type/logical safety checks. - var apiErr *requests.APIError - if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + if kosli.IsNotFound(err) { logger.Debug("environment %s does not exist yet", envName) return false, "", nil } return false, "", err } - var env map[string]any - if err := json.Unmarshal([]byte(response.Body), &env); err != nil { + environment, err := response.Value() + if err != nil { return false, "", err } - envType, _ := env["type"].(string) - return true, envType, nil + return true, environment.Type, nil } // createEnvironmentForSnapshot creates a physical environment of the inferred // type, applying the optional --environment-description and scaling flags. It // reuses the same payload and endpoint as `kosli create environment`. -func createEnvironmentForSnapshot(envName, envType string) error { - reqURL, err := url.JoinPath(global.Host, "api/v2/environments", global.Org) - if err != nil { - return err - } - +func createEnvironmentForSnapshot(ctx context.Context, envName, envType string) error { description := snapshotAutoEnv.description if description == "" { description = defaultAutoEnvDescription @@ -171,13 +150,7 @@ func createEnvironmentForSnapshot(envName, envType string) error { payload.IncludeScaling = &myFalse } - reqParams := &requests.RequestParams{ - Method: http.MethodPut, - URL: reqURL, - Payload: payload, - Token: global.ApiToken, - } - if _, err := kosliClient.Do(reqParams); err != nil { + if _, err := kosliAPI().CreateEnvironment(ctx, payload); err != nil { return fmt.Errorf("failed to auto-create environment %s: %v", envName, err) } logger.Info("environment %s was created", envName) diff --git a/cmd/kosli/snapshotAzureApps.go b/cmd/kosli/snapshotAzureApps.go index e35c862fa..aad25cf33 100644 --- a/cmd/kosli/snapshotAzureApps.go +++ b/cmd/kosli/snapshotAzureApps.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" @@ -99,7 +100,7 @@ func newSnapshotAzureAppsCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -123,9 +124,9 @@ func newSnapshotAzureAppsCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotAzureAppsOptions) run(args []string) error { +func (o *snapshotAzureAppsOptions) run(ctx context.Context, args []string) error { envName := args[0] - return newSnapshotReporter().report(envName, "azure-apps", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "azure-apps", func() (any, string, error) { webAppsData, err := o.azureStaticCredentials.GetAzureAppsData(logger) if err != nil { return nil, "", err diff --git a/cmd/kosli/snapshotCloudRun.go b/cmd/kosli/snapshotCloudRun.go index de02af796..8b2d3b0cf 100644 --- a/cmd/kosli/snapshotCloudRun.go +++ b/cmd/kosli/snapshotCloudRun.go @@ -124,7 +124,7 @@ func newSnapshotCloudRunCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -144,9 +144,9 @@ func newSnapshotCloudRunCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotCloudRunOptions) run(args []string) error { +func (o *snapshotCloudRunOptions) run(ctx context.Context, args []string) error { envName := args[0] - return newSnapshotReporter().report(envName, "cloud-run", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "cloud-run", func() (any, string, error) { // compile the filter patterns once, instead of once per service and job compiledFilter := o.resourceFilter.Compile() diff --git a/cmd/kosli/snapshotDocker.go b/cmd/kosli/snapshotDocker.go index 05d119636..dd4e91f9d 100644 --- a/cmd/kosli/snapshotDocker.go +++ b/cmd/kosli/snapshotDocker.go @@ -55,16 +55,16 @@ func newSnapshotDockerCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } addDryRunFlag(cmd) return cmd } -func (o *snapshotDockerOptions) run(args []string) error { +func (o *snapshotDockerOptions) run(ctx context.Context, args []string) error { envName := args[0] - return newSnapshotReporter().report(envName, "docker", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "docker", func() (any, string, error) { artifacts, err := CreateDockerArtifactsData() if err != nil { return nil, "", err diff --git a/cmd/kosli/snapshotECS.go b/cmd/kosli/snapshotECS.go index 34ec246ea..653061a43 100644 --- a/cmd/kosli/snapshotECS.go +++ b/cmd/kosli/snapshotECS.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" @@ -160,7 +161,7 @@ func newSnapshotECSCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -190,9 +191,9 @@ func newSnapshotECSCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotECSOptions) run(args []string) error { +func (o *snapshotECSOptions) run(ctx context.Context, args []string) error { envName := args[0] - return newSnapshotReporter().report(envName, "ECS", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "ECS", func() (any, string, error) { tasksData, err := o.awsStaticCreds.GetEcsTasksData(o.clustersFilter, o.serviceFilter, logger) if err != nil { return nil, "", err diff --git a/cmd/kosli/snapshotGKE.go b/cmd/kosli/snapshotGKE.go index 94d6121b6..2b3b35a8a 100644 --- a/cmd/kosli/snapshotGKE.go +++ b/cmd/kosli/snapshotGKE.go @@ -109,7 +109,7 @@ func newSnapshotGKECmd(out io.Writer) *cobra.Command { return o.validateRegexFlags() }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -157,9 +157,9 @@ func (o *snapshotGKEOptions) parent() string { } } -func (o *snapshotGKEOptions) run(args []string) error { +func (o *snapshotGKEOptions) run(ctx context.Context, args []string) error { envName := args[0] - return newSnapshotReporter().report(envName, "K8S", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "K8S", func() (any, string, error) { ctx := context.Background() client, err := newGKEClient(ctx) if err != nil { diff --git a/cmd/kosli/snapshotK8S.go b/cmd/kosli/snapshotK8S.go index e44ed488e..76d6d505e 100644 --- a/cmd/kosli/snapshotK8S.go +++ b/cmd/kosli/snapshotK8S.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "os" @@ -204,9 +205,9 @@ func newSnapshotK8SCmd(out io.Writer) *cobra.Command { }, RunE: func(cmd *cobra.Command, args []string) error { if o.configFilePath != "" { - return o.runMultiEnv() + return o.runMultiEnv(cmd.Context()) } - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -222,15 +223,15 @@ func newSnapshotK8SCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotK8SOptions) run(args []string) error { +func (o *snapshotK8SOptions) run(ctx context.Context, args []string) error { clientset, err := kube.NewK8sClientSet(o.kubeconfig) if err != nil { return err } - return o.reportEnvironment(newSnapshotReporter(), clientset, args[0], o.filter) + return o.reportEnvironment(ctx, newSnapshotReporter(), clientset, args[0], o.filter) } -func (o *snapshotK8SOptions) runMultiEnv() error { +func (o *snapshotK8SOptions) runMultiEnv(ctx context.Context) error { config, err := parseK8SSnapshotConfig(o.configFilePath) if err != nil { return err @@ -244,7 +245,7 @@ func (o *snapshotK8SOptions) runMultiEnv() error { reporter := newSnapshotReporter() var errs []string for _, env := range config.Environments { - if err := o.reportEnvironment(reporter, clientset, env.Name, env.toFilter()); err != nil { + if err := o.reportEnvironment(ctx, reporter, clientset, env.Name, env.toFilter()); err != nil { errs = append(errs, fmt.Sprintf("environment '%s': %v", env.Name, err)) } } @@ -255,8 +256,8 @@ func (o *snapshotK8SOptions) runMultiEnv() error { return nil } -func (o *snapshotK8SOptions) reportEnvironment(reporter *snapshotReporter, clientset *kube.K8SConnection, envName string, filter *filters.ResourceFilterOptions) error { - return reporter.report(envName, "K8S", func() (any, string, error) { +func (o *snapshotK8SOptions) reportEnvironment(ctx context.Context, reporter *snapshotReporter, clientset *kube.K8SConnection, envName string, filter *filters.ResourceFilterOptions) error { + return reporter.report(ctx, envName, "K8S", func() (any, string, error) { podsData, err := clientset.GetPodsData(filter, logger) if err != nil { return nil, "", err diff --git a/cmd/kosli/snapshotLambda.go b/cmd/kosli/snapshotLambda.go index 245a7042d..68788405b 100644 --- a/cmd/kosli/snapshotLambda.go +++ b/cmd/kosli/snapshotLambda.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" @@ -120,7 +121,7 @@ func newSnapshotLambdaCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -144,9 +145,9 @@ func newSnapshotLambdaCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotLambdaOptions) run(args []string) error { +func (o *snapshotLambdaOptions) run(ctx context.Context, args []string) error { envName := args[0] - return newSnapshotReporter().report(envName, "lambda", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "lambda", func() (any, string, error) { lambdaData, err := o.awsStaticCreds.GetLambdaPackageData(o.filter) if err != nil { return nil, "", err diff --git a/cmd/kosli/snapshotPath.go b/cmd/kosli/snapshotPath.go index 35f40f080..6359162a6 100644 --- a/cmd/kosli/snapshotPath.go +++ b/cmd/kosli/snapshotPath.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "os" @@ -64,7 +65,7 @@ func newSnapshotPathCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -81,7 +82,7 @@ func newSnapshotPathCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotPathOptions) run(args []string) error { +func (o *snapshotPathOptions) run(ctx context.Context, args []string) error { envName := args[0] // load path spec from flags @@ -96,13 +97,13 @@ func (o *snapshotPathOptions) run(args []string) error { } reporter := newSnapshotReporter() - err := reportArtifacts(reporter, ps, envName) + err := reportArtifacts(ctx, reporter, ps, envName) if err != nil { return err } if o.watch { - err := watchPath(reporter, ps, o.path, envName) + err := watchPath(ctx, reporter, ps, o.path, envName) if err != nil { return err } @@ -111,8 +112,8 @@ func (o *snapshotPathOptions) run(args []string) error { return nil } -func reportArtifacts(reporter *snapshotReporter, ps *server.PathsSpec, envName string) error { - return reporter.report(envName, "server", func() (any, string, error) { +func reportArtifacts(ctx context.Context, reporter *snapshotReporter, ps *server.PathsSpec, envName string) error { + return reporter.report(ctx, envName, "server", func() (any, string, error) { artifacts, err := server.CreatePathsArtifactsData(ps, logger) if err != nil { return nil, "", err @@ -122,7 +123,7 @@ func reportArtifacts(reporter *snapshotReporter, ps *server.PathsSpec, envName s }) } -func watchPath(reporter *snapshotReporter, ps *server.PathsSpec, path, envName string) error { +func watchPath(ctx context.Context, reporter *snapshotReporter, ps *server.PathsSpec, path, envName string) error { events := make(chan notify.EventInfo, 1) if err := watchRecursive(path, events); err != nil { return fmt.Errorf("error setting up watcher: %v", err) @@ -140,7 +141,7 @@ func watchPath(reporter *snapshotReporter, ps *server.PathsSpec, path, envName s case event := <-events: logger.Info("event detected: %s on %s", event.Event().String(), event.Path()) - err := reportArtifacts(reporter, ps, envName) + err := reportArtifacts(ctx, reporter, ps, envName) if err != nil { return err } diff --git a/cmd/kosli/snapshotPaths.go b/cmd/kosli/snapshotPaths.go index cae2ef3b8..a1f60bc61 100644 --- a/cmd/kosli/snapshotPaths.go +++ b/cmd/kosli/snapshotPaths.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "path/filepath" @@ -72,7 +73,7 @@ func newSnapshotPathsCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -87,7 +88,7 @@ func newSnapshotPathsCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotPathsOptions) run(args []string) error { +func (o *snapshotPathsOptions) run(ctx context.Context, args []string) error { envName := args[0] // load path spec from file @@ -97,7 +98,7 @@ func (o *snapshotPathsOptions) run(args []string) error { } reporter := newSnapshotReporter() - err = reportArtifacts(reporter, ps, envName) + err = reportArtifacts(ctx, reporter, ps, envName) if err != nil { return err } @@ -108,7 +109,7 @@ func (o *snapshotPathsOptions) run(args []string) error { wg.Add(1) go func(path string) { defer wg.Done() - err := watchPath(reporter, ps, path, envName) + err := watchPath(ctx, reporter, ps, path, envName) if err != nil { logger.Error("error watching path %s: %v", path, err) } diff --git a/cmd/kosli/snapshotReport.go b/cmd/kosli/snapshotReport.go index 105701083..5b82a50ad 100644 --- a/cmd/kosli/snapshotReport.go +++ b/cmd/kosli/snapshotReport.go @@ -1,25 +1,22 @@ package main import ( - "net/http" - "net/url" + "context" "sync" + "github.com/kosli-dev/cli/internal/kosli" log "github.com/kosli-dev/cli/internal/logger" - "github.com/kosli-dev/cli/internal/requests" ) // snapshotReporter reports Snapshots of Environments to Kosli. type snapshotReporter struct { - client *requests.Client - host string - org string - token string + api *kosli.Client + // dryRun only silences the success line: api sends nothing in a dry run. dryRun bool logger *log.Logger // ensure prepares the Environment before its first report, e.g. by // creating it under --auto-environment. - ensure func(envName, envType string) error + ensure func(ctx context.Context, envName, envType string) error // ensured holds the Environment name and type pairs already prepared, so // watch mode and multi-environment runs prepare each one only before its // first report. A failed ensure is not recorded, so the next report retries. @@ -32,10 +29,7 @@ type snapshotReporter struct { func newSnapshotReporter() *snapshotReporter { return &snapshotReporter{ - client: kosliClient, - host: global.Host, - org: global.Org, - token: global.ApiToken, + api: kosliAPI(), dryRun: global.DryRun, logger: logger, ensure: ensureEnvironment, @@ -46,13 +40,8 @@ func newSnapshotReporter() *snapshotReporter { // Environment type, and the line to log once Kosli has recorded it. type snapshotCollector func() (payload any, reported string, err error) -func (r *snapshotReporter) report(envName, envType string, collect snapshotCollector) error { - if err := r.ensureOnce(envName, envType); err != nil { - return err - } - - endpoint, err := url.JoinPath(r.host, "api/v2/environments", r.org, envName, "report", envType) - if err != nil { +func (r *snapshotReporter) report(ctx context.Context, envName, envType string, collect snapshotCollector) error { + if err := r.ensureOnce(ctx, envName, envType); err != nil { return err } @@ -61,27 +50,21 @@ func (r *snapshotReporter) report(envName, envType string, collect snapshotColle return err } - _, err = r.client.Do(&requests.RequestParams{ - Method: http.MethodPut, - URL: endpoint, - Payload: payload, - DryRun: r.dryRun, - Token: r.token, - }) + _, err = r.api.ReportSnapshot(ctx, envName, envType, payload) if err == nil && !r.dryRun { r.logger.Info("%s", reported) } return err } -func (r *snapshotReporter) ensureOnce(envName, envType string) error { +func (r *snapshotReporter) ensureOnce(ctx context.Context, envName, envType string) error { r.mu.Lock() defer r.mu.Unlock() key := envName + "/" + envType if r.ensured[key] { return nil } - if err := r.ensure(envName, envType); err != nil { + if err := r.ensure(ctx, envName, envType); err != nil { return err } if r.ensured == nil { diff --git a/cmd/kosli/snapshotReport_test.go b/cmd/kosli/snapshotReport_test.go index 988a8cb36..192c0e818 100644 --- a/cmd/kosli/snapshotReport_test.go +++ b/cmd/kosli/snapshotReport_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -11,6 +12,7 @@ import ( "sync" "testing" + "github.com/kosli-dev/cli/internal/kosli" log "github.com/kosli-dev/cli/internal/logger" "github.com/kosli-dev/cli/internal/requests" "github.com/stretchr/testify/assert" @@ -67,7 +69,7 @@ type ensureCalls struct { err error } -func (e *ensureCalls) ensure(envName, envType string) error { +func (e *ensureCalls) ensure(_ context.Context, envName, envType string) error { e.calls = append(e.calls, envName+"/"+envType) return e.err } @@ -78,12 +80,13 @@ func newTestSnapshotReporter(t *testing.T, host string, dryRun bool) (*snapshotR l := log.NewLogger(&out, &out, false) client, err := requests.NewKosliClient("", 0, false, l) require.NoError(t, err) + var sender kosli.Sender = client + if dryRun { + sender = kosli.DryRun(sender) + } ensured := &ensureCalls{} return &snapshotReporter{ - client: client, - host: host, - org: "acme", - token: "secret", + api: kosli.New(sender, host, "acme", "secret"), dryRun: dryRun, logger: l, ensure: ensured.ensure, @@ -99,7 +102,7 @@ func TestSnapshotReporterPutsThePayloadToTheEnvironmentTypeURL(t *testing.T) { server, fake := newFakeSnapshotServer(t) reporter, _, _ := newTestSnapshotReporter(t, server.URL, false) - err := reporter.report("prod", "docker", twoContainers) + err := reporter.report(context.Background(), "prod", "docker", twoContainers) require.NoError(t, err) require.Len(t, fake.recorded, 1) @@ -113,7 +116,7 @@ func TestSnapshotReporterEnsuresTheEnvironmentBeforeCollecting(t *testing.T) { reporter, ensured, _ := newTestSnapshotReporter(t, server.URL, false) var ensuredWhenCollecting []string - err := reporter.report("prod", "docker", func() (any, string, error) { + err := reporter.report(context.Background(), "prod", "docker", func() (any, string, error) { ensuredWhenCollecting = append([]string{}, ensured.calls...) return twoContainers() }) @@ -128,7 +131,7 @@ func TestSnapshotReporterStopsWhenTheEnvironmentCannotBeEnsured(t *testing.T) { ensured.err = errors.New("environment prod already exists with type K8S") collected := false - err := reporter.report("prod", "docker", func() (any, string, error) { + err := reporter.report(context.Background(), "prod", "docker", func() (any, string, error) { collected = true return twoContainers() }) @@ -142,7 +145,7 @@ func TestSnapshotReporterSendsNothingWhenCollectingFails(t *testing.T) { server, fake := newFakeSnapshotServer(t) reporter, _, out := newTestSnapshotReporter(t, server.URL, false) - err := reporter.report("prod", "docker", func() (any, string, error) { + err := reporter.report(context.Background(), "prod", "docker", func() (any, string, error) { return nil, "", errors.New("cannot reach docker daemon") }) @@ -155,7 +158,7 @@ func TestSnapshotReporterLogsTheReportedLine(t *testing.T) { server, _ := newFakeSnapshotServer(t) reporter, _, out := newTestSnapshotReporter(t, server.URL, false) - err := reporter.report("prod", "docker", twoContainers) + err := reporter.report(context.Background(), "prod", "docker", twoContainers) require.NoError(t, err) require.Equal(t, "[2] containers were reported to environment prod\n", out.String()) @@ -165,7 +168,7 @@ func TestSnapshotReporterSendsNothingAndLogsNoSuccessInDryRun(t *testing.T) { server, fake := newFakeSnapshotServer(t) reporter, _, out := newTestSnapshotReporter(t, server.URL, true) - err := reporter.report("prod", "docker", twoContainers) + err := reporter.report(context.Background(), "prod", "docker", twoContainers) require.NoError(t, err) require.Empty(t, fake.recorded) @@ -179,7 +182,7 @@ func TestSnapshotReporterReturnsTheServerErrorAndLogsNoSuccess(t *testing.T) { fake.body = `{"message":"Environment named 'prod' does not exist"}` reporter, _, out := newTestSnapshotReporter(t, server.URL, false) - err := reporter.report("prod", "docker", twoContainers) + err := reporter.report(context.Background(), "prod", "docker", twoContainers) require.EqualError(t, err, "Environment named 'prod' does not exist") require.Empty(t, out.String()) @@ -189,9 +192,9 @@ func TestSnapshotReporterEnsuresEachEnvironmentOnce(t *testing.T) { server, fake := newFakeSnapshotServer(t) reporter, ensured, _ := newTestSnapshotReporter(t, server.URL, false) - require.NoError(t, reporter.report("prod", "server", twoContainers)) - require.NoError(t, reporter.report("prod", "server", twoContainers)) - require.NoError(t, reporter.report("staging", "server", twoContainers)) + require.NoError(t, reporter.report(context.Background(), "prod", "server", twoContainers)) + require.NoError(t, reporter.report(context.Background(), "prod", "server", twoContainers)) + require.NoError(t, reporter.report(context.Background(), "staging", "server", twoContainers)) require.Equal(t, []string{"prod/server", "staging/server"}, ensured.calls) require.Len(t, fake.recorded, 3) @@ -203,16 +206,16 @@ func TestSnapshotReporterIsSafeForConcurrentReports(t *testing.T) { server, _ := newFakeSnapshotServer(t) reporter, ensured, _ := newTestSnapshotReporter(t, server.URL, false) var mu sync.Mutex - reporter.ensure = func(envName, envType string) error { + reporter.ensure = func(ctx context.Context, envName, envType string) error { mu.Lock() defer mu.Unlock() - return ensured.ensure(envName, envType) + return ensured.ensure(ctx, envName, envType) } var wg sync.WaitGroup for range 8 { wg.Go(func() { - assert.NoError(t, reporter.report("prod", "server", twoContainers)) + assert.NoError(t, reporter.report(context.Background(), "prod", "server", twoContainers)) }) } wg.Wait() @@ -224,8 +227,8 @@ func TestSnapshotReporterEnsuresAnEnvironmentAgainForAnotherType(t *testing.T) { server, _ := newFakeSnapshotServer(t) reporter, ensured, _ := newTestSnapshotReporter(t, server.URL, false) - require.NoError(t, reporter.report("prod", "server", twoContainers)) - require.NoError(t, reporter.report("prod", "docker", twoContainers)) + require.NoError(t, reporter.report(context.Background(), "prod", "server", twoContainers)) + require.NoError(t, reporter.report(context.Background(), "prod", "docker", twoContainers)) require.Equal(t, []string{"prod/server", "prod/docker"}, ensured.calls) } @@ -235,9 +238,9 @@ func TestSnapshotReporterRetriesAnEnvironmentItCouldNotEnsure(t *testing.T) { reporter, ensured, _ := newTestSnapshotReporter(t, server.URL, false) ensured.err = errors.New("network unreachable") - require.EqualError(t, reporter.report("prod", "server", twoContainers), "network unreachable") + require.EqualError(t, reporter.report(context.Background(), "prod", "server", twoContainers), "network unreachable") ensured.err = nil - require.NoError(t, reporter.report("prod", "server", twoContainers)) + require.NoError(t, reporter.report(context.Background(), "prod", "server", twoContainers)) require.Equal(t, []string{"prod/server", "prod/server"}, ensured.calls) require.Len(t, fake.recorded, 1) diff --git a/cmd/kosli/snapshotS3.go b/cmd/kosli/snapshotS3.go index bf1870216..c5e11fc44 100644 --- a/cmd/kosli/snapshotS3.go +++ b/cmd/kosli/snapshotS3.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" @@ -138,7 +139,7 @@ func newSnapshotS3Cmd(out io.Writer) *cobra.Command { return o.resolveDownloadLimits(cmd.Flags().Changed("download-concurrency")) }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -161,14 +162,14 @@ func newSnapshotS3Cmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotS3Options) run(args []string) error { +func (o *snapshotS3Options) run(ctx context.Context, args []string) error { envName := args[0] harvest := o.awsStaticCreds.GetS3Data if o.fingerprintSource == fingerprintSourceMetadata { harvest = o.awsStaticCreds.GetS3DataFromMetadata } - return newSnapshotReporter().report(envName, "S3", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "S3", func() (any, string, error) { s3Data, err := harvest(o.bucket, o.includePaths, o.includeRegex, o.excludePaths, o.excludeRegex, o.downloadLimits, logger) if err != nil { return nil, "", err diff --git a/cmd/kosli/snapshotServer.go b/cmd/kosli/snapshotServer.go index f1a4abf26..86c0c3673 100644 --- a/cmd/kosli/snapshotServer.go +++ b/cmd/kosli/snapshotServer.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" @@ -67,7 +68,7 @@ func newSnapshotServerCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(args) + return o.run(cmd.Context(), args) }, } @@ -91,9 +92,9 @@ func newSnapshotServerCmd(out io.Writer) *cobra.Command { return cmd } -func (o *snapshotServerOptions) run(args []string) error { +func (o *snapshotServerOptions) run(ctx context.Context, args []string) error { envName := args[0] - return newSnapshotReporter().report(envName, "server", func() (any, string, error) { + return newSnapshotReporter().report(ctx, envName, "server", func() (any, string, error) { artifacts, err := server.CreateServerArtifactsData(o.paths, o.excludePaths, logger) if err != nil { return nil, "", err diff --git a/cmd/kosli/testHelpers.go b/cmd/kosli/testHelpers.go index f3bbd1d4c..4f8fbdada 100644 --- a/cmd/kosli/testHelpers.go +++ b/cmd/kosli/testHelpers.go @@ -508,7 +508,7 @@ func ReportServerArtifactToEnv(paths []string, envName string, t *testing.T) { o := &snapshotServerOptions{ paths: paths, } - err := o.run([]string{envName}) + err := o.run(context.Background(), []string{envName}) require.NoError(t, err, "server env should be reported without error") } From 8438f1d863a0d4b0984b3589a814421b48d663e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Thu, 15 Oct 2026 17:07:57 +0200 Subject: [PATCH 5/5] refactor(kosli): rename to CreateOrUpdateEnvironment and mark its deprecated fields The method calls create_or_update_environment and overwrites an existing environment, so its name now says so. IncludeScaling and RequireProvenance are deprecated on the server (accepted, ignored); the deprecated flags that set them keep working, behind a reasoned nolint. --- cmd/kosli/createEnvironment.go | 10 +++++----- cmd/kosli/openapiContract_test.go | 2 +- cmd/kosli/snapshotAutoEnvironment.go | 8 ++++---- cmd/kosli/snapshotAutoEnvironment_test.go | 2 +- cmd/kosli/testHelpers.go | 2 +- internal/kosli/environments.go | 23 ++++++++++++++--------- internal/kosli/environments_test.go | 6 +++--- 7 files changed, 29 insertions(+), 24 deletions(-) diff --git a/cmd/kosli/createEnvironment.go b/cmd/kosli/createEnvironment.go index 5de34c717..8a3cee752 100644 --- a/cmd/kosli/createEnvironment.go +++ b/cmd/kosli/createEnvironment.go @@ -49,7 +49,7 @@ kosli create environment yourLogicalEnvironmentName ` type createEnvOptions struct { - payload kosli.CreateEnvironmentRequest + payload kosli.CreateOrUpdateEnvironmentRequest excludeScaling bool includeScaling bool requireProvenance bool @@ -112,18 +112,18 @@ func (o *createEnvOptions) run(ctx context.Context, args []string) error { if o.includeScaling { var myTrue = true - o.payload.IncludeScaling = &myTrue + o.payload.IncludeScaling = &myTrue //nolint:staticcheck // set only by the deprecated scaling flags } if o.excludeScaling { var myFalse = false - o.payload.IncludeScaling = &myFalse + o.payload.IncludeScaling = &myFalse //nolint:staticcheck // set only by the deprecated scaling flags } if o.requireProvenance { var myTrue = true - o.payload.RequireProvenance = &myTrue + o.payload.RequireProvenance = &myTrue //nolint:staticcheck // set only by the deprecated --require-provenance flag } - _, err := kosliAPI().CreateEnvironment(ctx, o.payload) + _, err := kosliAPI().CreateOrUpdateEnvironment(ctx, o.payload) if err == nil && !global.DryRun { logger.Info("environment %s was created", o.payload.Name) } diff --git a/cmd/kosli/openapiContract_test.go b/cmd/kosli/openapiContract_test.go index 6d46ee20a..01bb4b68e 100644 --- a/cmd/kosli/openapiContract_test.go +++ b/cmd/kosli/openapiContract_test.go @@ -67,7 +67,7 @@ func (suite *OpenAPIContractTestSuite) SetupSuite() { func (suite *OpenAPIContractTestSuite) TestPayloadsMatchSchema() { registry := []driftCase{ {name: "create control", payload: ControlPayload{}, component: "ControlPostInput"}, - {name: "create environment", payload: kosli.CreateEnvironmentRequest{}, component: "CreateEnvironmentPutInput"}, + {name: "create environment", payload: kosli.CreateOrUpdateEnvironmentRequest{}, component: "CreateEnvironmentPutInput"}, } for _, c := range registry { diff --git a/cmd/kosli/snapshotAutoEnvironment.go b/cmd/kosli/snapshotAutoEnvironment.go index 254d802fa..e92c53f77 100644 --- a/cmd/kosli/snapshotAutoEnvironment.go +++ b/cmd/kosli/snapshotAutoEnvironment.go @@ -136,21 +136,21 @@ func createEnvironmentForSnapshot(ctx context.Context, envName, envType string) description = defaultAutoEnvDescription } - payload := kosli.CreateEnvironmentRequest{ + payload := kosli.CreateOrUpdateEnvironmentRequest{ Name: envName, Type: envType, Description: description, } if snapshotAutoEnv.includeScaling { myTrue := true - payload.IncludeScaling = &myTrue + payload.IncludeScaling = &myTrue //nolint:staticcheck // set only by the deprecated scaling flags } if snapshotAutoEnv.excludeScaling { myFalse := false - payload.IncludeScaling = &myFalse + payload.IncludeScaling = &myFalse //nolint:staticcheck // set only by the deprecated scaling flags } - if _, err := kosliAPI().CreateEnvironment(ctx, payload); err != nil { + if _, err := kosliAPI().CreateOrUpdateEnvironment(ctx, payload); err != nil { return fmt.Errorf("failed to auto-create environment %s: %v", envName, err) } logger.Info("environment %s was created", envName) diff --git a/cmd/kosli/snapshotAutoEnvironment_test.go b/cmd/kosli/snapshotAutoEnvironment_test.go index edc4b5d73..638feb2be 100644 --- a/cmd/kosli/snapshotAutoEnvironment_test.go +++ b/cmd/kosli/snapshotAutoEnvironment_test.go @@ -55,7 +55,7 @@ func (suite *SnapshotAutoEnvironmentTestSuite) SetupSuite() { func (suite *SnapshotAutoEnvironmentTestSuite) createLogicalEnv(name string, included []string) { suite.T().Helper() o := &createEnvOptions{ - payload: kosli.CreateEnvironmentRequest{ + payload: kosli.CreateOrUpdateEnvironmentRequest{ Name: name, Type: "logical", Description: "test logical env", diff --git a/cmd/kosli/testHelpers.go b/cmd/kosli/testHelpers.go index 4f8fbdada..ab9a0b9b0 100644 --- a/cmd/kosli/testHelpers.go +++ b/cmd/kosli/testHelpers.go @@ -491,7 +491,7 @@ func EnableBeta(t *testing.T) { func CreateEnv(org, envName, envType string, t *testing.T) { t.Helper() o := &createEnvOptions{ - payload: kosli.CreateEnvironmentRequest{ + payload: kosli.CreateOrUpdateEnvironmentRequest{ Name: envName, Type: envType, Description: "test env", diff --git a/internal/kosli/environments.go b/internal/kosli/environments.go index 88b8613cf..ede233df7 100644 --- a/internal/kosli/environments.go +++ b/internal/kosli/environments.go @@ -26,19 +26,24 @@ func (c *Client) GetEnvironment(ctx context.Context, name string) (*Response[Env return &Response[Environment]{Result: *result}, nil } -// CreateEnvironmentRequest describes the Environment CreateEnvironment creates. -type CreateEnvironmentRequest struct { - Name string `json:"name"` - Type string `json:"type"` - Description string `json:"description"` - IncludeScaling *bool `json:"include_scaling,omitempty"` +// CreateOrUpdateEnvironmentRequest describes the Environment that +// CreateOrUpdateEnvironment creates or updates. +type CreateOrUpdateEnvironmentRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + // Deprecated: the server accepts but ignores it, since scaling events are + // no longer recorded. It is sent only for the deprecated scaling flags. + IncludeScaling *bool `json:"include_scaling,omitempty"` + // Deprecated: the server accepts but ignores it; use policies instead. It is + // sent only for the deprecated --require-provenance flag. RequireProvenance *bool `json:"require_provenance,omitempty"` IncludedEnvironments []string `json:"included_environments,omitempty"` } -// CreateEnvironment calls create_or_update_environment. It creates the -// Environment, or updates it when one with that name already exists. -func (c *Client) CreateEnvironment(ctx context.Context, request CreateEnvironmentRequest) (*Result, error) { +// CreateOrUpdateEnvironment calls create_or_update_environment. It creates the +// Environment, or overwrites the one that already has that name. +func (c *Client) CreateOrUpdateEnvironment(ctx context.Context, request CreateOrUpdateEnvironmentRequest) (*Result, error) { endpoint, err := c.endpoint("environments", c.org) if err != nil { return nil, err diff --git a/internal/kosli/environments_test.go b/internal/kosli/environments_test.go index 4dbe7e80a..7231746e6 100644 --- a/internal/kosli/environments_test.go +++ b/internal/kosli/environments_test.go @@ -31,12 +31,12 @@ func TestGetEnvironmentOfAMissingEnvironmentIsNotFound(t *testing.T) { require.True(t, IsNotFound(err)) } -func TestCreateEnvironmentPutsTheRequestToTheOrgsEnvironments(t *testing.T) { +func TestCreateOrUpdateEnvironmentPutsTheRequestToTheOrgsEnvironments(t *testing.T) { server, seen := newFakeServer(t, http.StatusCreated, ``) client := newTestClient(t, server.URL) include := true - result, err := client.CreateEnvironment(context.Background(), CreateEnvironmentRequest{ + result, err := client.CreateOrUpdateEnvironment(context.Background(), CreateOrUpdateEnvironmentRequest{ Name: "prod", Type: "K8S", Description: "production", IncludeScaling: &include, }) @@ -63,7 +63,7 @@ func TestADryRunSendsNeitherEnvironmentWrite(t *testing.T) { server, seen := newFakeServer(t, http.StatusCreated, ``) client := New(DryRun(newHTTPClient(t)), server.URL, "test-org", "test-token") - _, err := client.CreateEnvironment(context.Background(), CreateEnvironmentRequest{Name: "prod", Type: "K8S"}) + _, err := client.CreateOrUpdateEnvironment(context.Background(), CreateOrUpdateEnvironmentRequest{Name: "prod", Type: "K8S"}) require.NoError(t, err) _, err = client.ReportSnapshot(context.Background(), "prod", "K8S", map[string]any{}) require.NoError(t, err)