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
6 changes: 6 additions & 0 deletions e2e/openapi-cfg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ output-options:
- createProject
- createKubernetesPamResource
- createRedisPamResource
- deleteSecretV4
- updateSecretV4
- createSecretV4
- getSecretByNameV4
- listSecretsV4

4,858 changes: 3,501 additions & 1,357 deletions e2e/packages/client/client.gen.go

Large diffs are not rendered by default.

72 changes: 70 additions & 2 deletions e2e/packages/infisical/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/compose"
"github.com/testcontainers/testcontainers-go/wait"
Expand Down Expand Up @@ -144,6 +145,20 @@ func (s *Stack) Down(ctx context.Context) error {
return s.dockerCompose.Down(ctx)
}

// DownWithForce tears down all containers and optionally removes volumes.
// This works even when using container reuse (RunningCompose).
func (s *Stack) DownWithForce(ctx context.Context, removeVolumes bool) error {
if rc, ok := s.dockerCompose.(*RunningCompose); ok {
return rc.DownWithForce(ctx, removeVolumes)
}
// For regular compose stacks, use the standard Down with options
opts := []compose.StackDownOption{compose.RemoveOrphans(true)}
if removeVolumes {
opts = append(opts, compose.RemoveVolumes(true))
}
return s.dockerCompose.Down(ctx, opts...)
}

func (s *Stack) Compose() compose.ComposeStack {
return s.dockerCompose
}
Expand Down Expand Up @@ -236,8 +251,8 @@ func WithBackendService(options BackendOptions) StackOption {
Dockerfile: dockerfile,
},
Ports: []types.ServicePortConfig{
{Published: "4000", Target: 4000},
{Published: "9229", Target: 9229},
{Target: 4000}, // Let Docker assign a random host port to avoid conflicts
{Target: 9229},
},
Environment: types.NewMappingWithEquals([]string{
"NODE_ENV=development",
Expand Down Expand Up @@ -301,6 +316,59 @@ func (c *RunningCompose) Down(ctx context.Context, opts ...compose.StackDownOpti
return nil
}

// DownWithForce tears down all containers and optionally removes volumes.
// Unlike Down(), this actually removes containers even when using RunningCompose.
func (c *RunningCompose) DownWithForce(ctx context.Context, removeVolumes bool) error {
slog.Info("Force tearing down compose stack", "name", c.name, "removeVolumes", removeVolumes)

containers, err := c.client.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", fmt.Sprintf("%s=%s", api.ProjectLabel, c.name)),
),
})
if err != nil {
return fmt.Errorf("container list: %w", err)
}

// Stop and remove all containers
for _, ctr := range containers {
slog.Info("Stopping and removing container", "id", ctr.ID[:12], "name", ctr.Names)
timeout := 10
if err := c.client.ContainerStop(ctx, ctr.ID, container.StopOptions{Timeout: &timeout}); err != nil {
slog.Warn("Failed to stop container", "id", ctr.ID[:12], "error", err)
}
if err := c.client.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{Force: true, RemoveVolumes: removeVolumes}); err != nil {
slog.Warn("Failed to remove container", "id", ctr.ID[:12], "error", err)
}
}

// Remove the network
networks, err := c.client.NetworkList(ctx, network.ListOptions{
Filters: filters.NewArgs(
filters.Arg("label", fmt.Sprintf("%s=%s", api.ProjectLabel, c.name)),
),
})
if err != nil {
slog.Warn("Failed to list networks", "error", err)
} else {
for _, network := range networks {
slog.Info("Removing network", "name", network.Name)
if err := c.client.NetworkRemove(ctx, network.ID); err != nil {
slog.Warn("Failed to remove network", "name", network.Name, "error", err)
}
}
}

// Clear the cached containers
c.containersLock.Lock()
c.containers = make(map[string]*testcontainers.DockerContainer)
c.containersLock.Unlock()

slog.Info("Compose stack torn down", "name", c.name)
return nil
}

func (c *RunningCompose) Services() []string {
return c.services
}
Expand Down
174 changes: 174 additions & 0 deletions e2e/proxy/proxy_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package proxy

import (
"context"
"fmt"
"log/slog"
"net/http"
"testing"
"time"

"github.com/go-faker/faker/v4"
"github.com/infisical/cli/e2e-tests/packages/client"
"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
"github.com/stretchr/testify/require"
)

// ProxyTestHelper provides helper methods for proxy tests
type ProxyTestHelper struct {
T *testing.T
ProxyClient client.ClientWithResponsesInterface // client pointing to proxy
ApiClient client.ClientWithResponsesInterface // client pointing to Infisical directly
ProjectID string
Environment string
}

type Secret struct {
SecretKey string
SecretValue string
}

// NewProxyTestHelper creates a new test helper with clients for both proxy and direct API access
func NewProxyTestHelper(t *testing.T, proxyURL, infisicalURL, identityToken, projectID string) *ProxyTestHelper {
bearerAuth, err := securityprovider.NewSecurityProviderBearerToken(identityToken)
require.NoError(t, err)

// client for requests through the proxy (to test caching)
proxyClient, err := client.NewClientWithResponses(
proxyURL,
client.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
client.WithRequestEditorFn(bearerAuth.Intercept),
)
require.NoError(t, err)

// client for direct API access
apiClient, err := client.NewClientWithResponses(
infisicalURL,
client.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
client.WithRequestEditorFn(bearerAuth.Intercept),
)
require.NoError(t, err)

return &ProxyTestHelper{
T: t,
ProxyClient: proxyClient,
ApiClient: apiClient,
ProjectID: projectID,
Environment: "dev",
}
}

// CreateSecretWithApi creates a secret directly through Infisical API (not through proxy)
func (h *ProxyTestHelper) CreateSecretWithApi(ctx context.Context, secret Secret) {
secretPath := "/"
resp, err := h.ApiClient.CreateSecretV4WithResponse(ctx, secret.SecretKey, client.CreateSecretV4JSONRequestBody{
ProjectId: h.ProjectID,
Environment: h.Environment,
SecretValue: secret.SecretValue,
SecretPath: &secretPath,
})
require.NoError(h.T, err)
require.Equal(h.T, http.StatusOK, resp.StatusCode(), "Failed to create secret: %s", string(resp.Body))
slog.Info("Created secret", "name", secret.SecretKey, "value", secret.SecretValue)
}

// UpdateSecretWithApi updates a secret directly through Infisical API (not through proxy)
func (h *ProxyTestHelper) UpdateSecretWithApi(ctx context.Context, secret Secret) {
secretPath := "/"
resp, err := h.ApiClient.UpdateSecretV4WithResponse(ctx, secret.SecretKey, client.UpdateSecretV4JSONRequestBody{
ProjectId: h.ProjectID,
Environment: h.Environment,
SecretValue: &secret.SecretValue,
SecretPath: &secretPath,
})
require.NoError(h.T, err)
require.Equal(h.T, http.StatusOK, resp.StatusCode(), "Failed to update secret: %s", string(resp.Body))
slog.Info("Updated secret directly", "name", secret.SecretKey, "newValue", secret.SecretValue)
}

// GetSecretsWithProxy fetches secrets through the proxy
func (h *ProxyTestHelper) GetSecretsWithProxy(ctx context.Context) *client.ListSecretsV4Response {
secretPath := "/"
projectID := h.ProjectID
environment := h.Environment
resp, err := h.ProxyClient.ListSecretsV4WithResponse(ctx, &client.ListSecretsV4Params{
ProjectId: &projectID,
Environment: &environment,
SecretPath: &secretPath,
})
require.NoError(h.T, err)
return resp
}

// GetSecretByNameWithProxy fetches a single secret through the proxy
func (h *ProxyTestHelper) GetSecretByNameWithProxy(ctx context.Context, secretName string) *client.GetSecretByNameV4Response {
secretPath := "/"
environment := h.Environment
resp, err := h.ProxyClient.GetSecretByNameV4WithResponse(ctx, secretName, &client.GetSecretByNameV4Params{
ProjectId: h.ProjectID,
Environment: &environment,
SecretPath: &secretPath,
})
require.NoError(h.T, err)
return resp
}

// UpdateSecretWithProxy updates a secret through the proxy (triggers mutation purging)
func (h *ProxyTestHelper) UpdateSecretWithProxy(ctx context.Context, secret Secret) *client.UpdateSecretV4Response {
secretPath := "/"
resp, err := h.ProxyClient.UpdateSecretV4WithResponse(ctx, secret.SecretKey, client.UpdateSecretV4JSONRequestBody{
ProjectId: h.ProjectID,
Environment: h.Environment,
SecretPath: &secretPath,
SecretValue: &secret.SecretValue,
})
require.NoError(h.T, err)
return resp
}

// DeleteSecretWithProxy deletes a secret through the proxy (triggers mutation purging)
func (h *ProxyTestHelper) DeleteSecretWithProxy(ctx context.Context, secretName string) *client.DeleteSecretV4Response {
secretPath := "/"
resp, err := h.ProxyClient.DeleteSecretV4WithResponse(ctx, secretName, client.DeleteSecretV4JSONRequestBody{
ProjectId: h.ProjectID,
Environment: h.Environment,
SecretPath: &secretPath,
})
require.NoError(h.T, err)
return resp
}

type GenerateSecretOptions struct {
// Prefix is only used if no PresetName is provided
Prefix string

PresetName string
PresetValue string
}

func (h *ProxyTestHelper) GenerateSecret(opts GenerateSecretOptions) Secret {

secretName := ""
secretValue := ""

if opts.PresetName != "" {
secretName = opts.PresetName
}
if opts.PresetValue != "" {
secretValue = opts.PresetValue
}

if secretName == "" {
secretName = fmt.Sprintf("%s%s", opts.Prefix, faker.Word())
}

if secretValue == "" {
secretValue = faker.Password()
}

return Secret{
SecretKey: secretName,
SecretValue: secretValue,
}

}
Loading
Loading