Files
coder/enterprise/cli/boundary_test.go
T
Sas Swart 52722b800b chore: rename boundary command to agent-firewall (#25889)
Renames the `coder boundary` CLI subcommand to `coder agent-firewall` as
part of the Boundaries → Agent Firewall rebrand.

`coder boundary` is retained as a hidden, deprecated alias that prints a
deprecation notice to stderr before running. Both commands use separate
builder functions backed by the same boundary base command and license
verification logic.

Closes https://linear.app/codercom/issue/AIGOV-236

<details><summary>Implementation notes</summary>

**Approach:** Two separate `*serpent.Command` objects (not `Aliases`) so
the deprecated `boundary` path can print a stderr warning while
`agent-firewall` stays clean.

**Changes:**
- `enterprise/cli/boundary.go`: Split old `boundary()` into
`buildAgentFirewallCmd()` and `buildBoundaryAliasCmd()`. Error messages
in `verifyLicense` now reference "agent-firewall".
- `enterprise/cli/root.go`: Register both commands.
- `cli/root.go`: Update YAML-only option validation bypass for the new
command name.
- Tests: Rename to `TestAgentFirewallSubcommand`, add
`TestBoundaryAlias`, update license verification tests to use
`agent-firewall`.
- Golden files and CLI reference docs regenerated.
- `docs/ai-coder/agent-firewall/version.md` and `docs/manifest.json`
updated.

</details>

> Generated with [Coder Agents](https://coder.com/agents) by @SasSwart
2026-06-04 11:14:36 +02:00

322 lines
10 KiB
Go

package cli_test
import (
"bytes"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
boundarycli "github.com/coder/boundary/cli"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)
// Actually testing the functionality of coder/boundary takes place in the
// coder/boundary repo, since it's a dependency of coder.
// Here we want to test basically that integrating it as a subcommand doesn't break anything.
func TestAgentFirewallSubcommand(t *testing.T) {
t.Parallel()
inv, _ := newCLI(t, "agent-firewall", "--help")
var buf bytes.Buffer
inv.Stdout = &buf
inv.Stderr = &buf
err := inv.Run()
require.NoError(t, err)
// Verify help output contains expected information.
// We're simply confirming that `coder agent-firewall --help` ran without a runtime error as
// a good chunk of serpent's self validation logic happens at runtime.
output := buf.String()
assert.Contains(t, output, boundarycli.BaseCommand("dev").Short)
}
func TestBoundaryAlias(t *testing.T) {
t.Parallel()
inv, _ := newCLI(t, "boundary", "--help")
var buf bytes.Buffer
inv.Stdout = &buf
inv.Stderr = &buf
err := inv.Run()
require.NoError(t, err)
// The alias should dispatch to the same command and display help.
output := buf.String()
assert.Contains(t, output, boundarycli.BaseCommand("dev").Short)
}
func TestAgentFirewallLicenseVerification(t *testing.T) {
t.Parallel()
t.Run("EntitledAndEnabled", func(t *testing.T) {
t.Parallel()
client, _ := coderdenttest.New(t, &coderdenttest.Options{
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureBoundary: 1,
},
},
})
inv, conf := newCLI(t, "agent-firewall", "--version")
//nolint:gocritic // requires owner
clitest.SetupConfig(t, client, conf)
ctx := testutil.Context(t, testutil.WaitShort)
err := inv.WithContext(ctx).Run()
// Should succeed - agent-firewall --version should work with valid license.
require.NoError(t, err)
})
t.Run("NotEntitled", func(t *testing.T) {
t.Parallel()
// Create a proxy server that returns entitlements without boundary feature.
client, _ := coderdenttest.New(t, &coderdenttest.Options{
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
// No FeatureBoundary
},
},
})
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/entitlements" {
res := codersdk.Entitlements{
Features: map[codersdk.FeatureName]codersdk.Feature{},
Warnings: []string{},
Errors: []string{},
HasLicense: true,
Trial: false,
RequireTelemetry: false,
}
// Set boundary to not entitled, all other features to entitled.
for _, feature := range codersdk.FeatureNames {
if feature == codersdk.FeatureBoundary {
// Explicitly set boundary to not entitled.
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementNotEntitled,
Enabled: false,
}
} else {
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementEntitled,
Enabled: true,
}
}
}
httpapi.Write(r.Context(), w, http.StatusOK, res)
return
}
// Otherwise, proxy the request to the real API server.
rp := httputil.NewSingleHostReverseProxy(client.URL)
tp := &http.Transport{}
defer tp.CloseIdleConnections()
rp.Transport = tp
rp.ServeHTTP(w, r)
}))
defer proxy.Close()
proxyURL, err := url.Parse(proxy.URL)
require.NoError(t, err)
proxyClient := codersdk.New(proxyURL, codersdk.WithHTTPClient(coderdtest.NewIsolatedHTTPClient(proxyURL)))
proxyClient.SetSessionToken(client.SessionToken())
t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
inv, conf := newCLI(t, "agent-firewall", "--version")
clitest.SetupConfig(t, proxyClient, conf)
ctx := testutil.Context(t, testutil.WaitShort)
err = inv.WithContext(ctx).Run()
require.Error(t, err)
require.ErrorContains(t, err, "your license is not entitled to use the agent-firewall feature")
})
t.Run("FeatureDisabled", func(t *testing.T) {
t.Parallel()
// Create a proxy server that returns entitlements with boundary disabled.
client, _ := coderdenttest.New(t, &coderdenttest.Options{
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureBoundary: 1,
},
},
})
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/entitlements" {
res := codersdk.Entitlements{
Features: map[codersdk.FeatureName]codersdk.Feature{},
Warnings: []string{},
Errors: []string{},
HasLicense: true,
Trial: false,
RequireTelemetry: false,
}
for _, feature := range codersdk.FeatureNames {
if feature == codersdk.FeatureBoundary {
// Feature is entitled but disabled.
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementEntitled,
Enabled: false,
}
} else {
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementEntitled,
Enabled: true,
}
}
}
httpapi.Write(r.Context(), w, http.StatusOK, res)
return
}
// Otherwise, proxy the request to the real API server.
rp := httputil.NewSingleHostReverseProxy(client.URL)
tp := &http.Transport{}
defer tp.CloseIdleConnections()
rp.Transport = tp
rp.ServeHTTP(w, r)
}))
defer proxy.Close()
proxyURL, err := url.Parse(proxy.URL)
require.NoError(t, err)
proxyClient := codersdk.New(proxyURL, codersdk.WithHTTPClient(coderdtest.NewIsolatedHTTPClient(proxyURL)))
proxyClient.SetSessionToken(client.SessionToken())
t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
inv, conf := newCLI(t, "agent-firewall", "--version")
clitest.SetupConfig(t, proxyClient, conf)
ctx := testutil.Context(t, testutil.WaitShort)
err = inv.WithContext(ctx).Run()
require.Error(t, err)
require.ErrorContains(t, err, "the agent-firewall feature is disabled in your deployment configuration")
})
t.Run("AGPLDeployment", func(t *testing.T) {
t.Parallel()
// Create an AGPL server (no enterprise features).
client := coderdtest.New(t, &coderdtest.Options{})
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/entitlements" {
// AGPL deployments return 404 for entitlements endpoint.
w.WriteHeader(http.StatusNotFound)
return
}
// Otherwise, proxy the request to the real API server.
rp := httputil.NewSingleHostReverseProxy(client.URL)
tp := &http.Transport{}
defer tp.CloseIdleConnections()
rp.Transport = tp
rp.ServeHTTP(w, r)
}))
defer proxy.Close()
proxyURL, err := url.Parse(proxy.URL)
require.NoError(t, err)
proxyClient := codersdk.New(proxyURL, codersdk.WithHTTPClient(coderdtest.NewIsolatedHTTPClient(proxyURL)))
proxyClient.SetSessionToken(client.SessionToken())
t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
inv, conf := newCLI(t, "agent-firewall", "--version")
clitest.SetupConfig(t, proxyClient, conf)
ctx := testutil.Context(t, testutil.WaitShort)
err = inv.WithContext(ctx).Run()
require.Error(t, err)
require.ErrorContains(t, err, "your deployment appears to be an AGPL deployment")
})
}
// TestAgentFirewallChildProcessSkipsCheck verifies that when CHILD=true, the
// license check is skipped. This simulates boundary re-executing itself to run
// the target process. We use a proxy that would fail the license check to
// verify it's skipped.
func TestAgentFirewallChildProcessSkipsCheck(t *testing.T) {
// Cannot use t.Parallel() with t.Setenv().
client, _ := coderdenttest.New(t, &coderdenttest.Options{
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
// No FeatureBoundary - would normally fail
},
},
})
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/entitlements" {
// Return not entitled for boundary - this would normally cause failure.
res := codersdk.Entitlements{
Features: map[codersdk.FeatureName]codersdk.Feature{},
Warnings: []string{},
Errors: []string{},
HasLicense: true,
Trial: false,
RequireTelemetry: false,
}
for _, feature := range codersdk.FeatureNames {
if feature == codersdk.FeatureBoundary {
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementNotEntitled,
Enabled: false,
}
} else {
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementEntitled,
Enabled: true,
}
}
}
httpapi.Write(r.Context(), w, http.StatusOK, res)
return
}
// Otherwise, proxy the request to the real API server.
rp := httputil.NewSingleHostReverseProxy(client.URL)
tp := &http.Transport{}
defer tp.CloseIdleConnections()
rp.Transport = tp
rp.ServeHTTP(w, r)
}))
defer proxy.Close()
proxyURL, err := url.Parse(proxy.URL)
require.NoError(t, err)
proxyClient := codersdk.New(proxyURL, codersdk.WithHTTPClient(coderdtest.NewIsolatedHTTPClient(proxyURL)))
proxyClient.SetSessionToken(client.SessionToken())
t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
inv, conf := newCLI(t, "agent-firewall", "--version")
clitest.SetupConfig(t, proxyClient, conf)
// Set CHILD=true to simulate boundary re-execution. This should skip the
// license check, so the command should succeed even though the proxy would
// return "not entitled".
t.Setenv("CHILD", "true")
ctx := testutil.Context(t, testutil.WaitShort)
err = inv.WithContext(ctx).Run()
// Should succeed because license check is skipped for child processes.
require.NoError(t, err)
}