mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: Refactor Enterprise code to layer on top of AGPL (#4034)
* chore: Refactor Enterprise code to layer on top of AGPL This is an experiment to invert the import order of the Enterprise code to layer on top of AGPL. * Fix Garrett's comments * Add pointer.Handle to atomically obtain references This uses a context to ensure the same value persists through multiple executions to `Load()`. * Remove entitlements API from AGPL coderd * Remove AGPL Coder entitlements endpoint test * Fix warnings output * Add command-line flag to toggle audit logging * Fix hasLicense being set * Remove features interface * Fix audit logging default * Add bash as a dependency * Add comment * Add tests for resync and pubsub, and add back previous exp backoff retry * Separate authz code again * Add pointer loading example from comment * Fix duplicate test, remove pointer.Handle * Fix expired license * Add entitlements struct * Fix context passing
This commit is contained in:
-104
@@ -1,104 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func features() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Short: "List Enterprise features",
|
||||
Use: "features",
|
||||
Aliases: []string{"feature"},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
featuresList(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func featuresList() *cobra.Command {
|
||||
var (
|
||||
featureColumns = []string{"Name", "Entitlement", "Enabled", "Limit", "Actual"}
|
||||
columns []string
|
||||
outputFormat string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := CreateClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entitlements, err := client.Entitlements(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := ""
|
||||
switch outputFormat {
|
||||
case "table", "":
|
||||
out, err = displayFeatures(columns, entitlements.Features)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("render table: %w", err)
|
||||
}
|
||||
case "json":
|
||||
buf := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetIndent("", " ")
|
||||
err = enc.Encode(entitlements)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal features to JSON: %w", err)
|
||||
}
|
||||
out = buf.String()
|
||||
default:
|
||||
return xerrors.Errorf(`unknown output format %q, only "table" and "json" are supported`, outputFormat)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringArrayVarP(&columns, "column", "c", featureColumns,
|
||||
fmt.Sprintf("Specify a column to filter in the table. Available columns are: %s",
|
||||
strings.Join(featureColumns, ", ")))
|
||||
cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "Output format. Available formats are: table, json.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
type featureRow struct {
|
||||
Name string `table:"name"`
|
||||
Entitlement string `table:"entitlement"`
|
||||
Enabled bool `table:"enabled"`
|
||||
Limit *int64 `table:"limit"`
|
||||
Actual *int64 `table:"actual"`
|
||||
}
|
||||
|
||||
// displayFeatures will return a table displaying all features passed in.
|
||||
// filterColumns must be a subset of the feature fields and will determine which
|
||||
// columns to display
|
||||
func displayFeatures(filterColumns []string, features map[string]codersdk.Feature) (string, error) {
|
||||
rows := make([]featureRow, 0, len(features))
|
||||
for name, feat := range features {
|
||||
rows = append(rows, featureRow{
|
||||
Name: name,
|
||||
Entitlement: string(feat.Entitlement),
|
||||
Enabled: feat.Enabled,
|
||||
Limit: feat.Limit,
|
||||
Actual: feat.Actual,
|
||||
})
|
||||
}
|
||||
|
||||
return cliui.DisplayTable(rows, "name", filterColumns)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
)
|
||||
|
||||
func TestFeaturesList(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Table", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
cmd, root := clitest.New(t, "features", "list")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetOut(pty.Output())
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
errC <- cmd.Execute()
|
||||
}()
|
||||
require.NoError(t, <-errC)
|
||||
pty.ExpectMatch("user_limit")
|
||||
pty.ExpectMatch("not_entitled")
|
||||
})
|
||||
t.Run("JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
cmd, root := clitest.New(t, "features", "list", "-o", "json")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
doneChan := make(chan struct{})
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
cmd.SetOut(buf)
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
<-doneChan
|
||||
|
||||
var entitlements codersdk.Entitlements
|
||||
err := json.Unmarshal(buf.Bytes(), &entitlements)
|
||||
require.NoError(t, err, "unmarshal JSON output")
|
||||
assert.Len(t, entitlements.Features, 2)
|
||||
assert.Empty(t, entitlements.Warnings)
|
||||
assert.Equal(t, codersdk.EntitlementNotEntitled,
|
||||
entitlements.Features[codersdk.FeatureUserLimit].Entitlement)
|
||||
assert.Equal(t, codersdk.EntitlementNotEntitled,
|
||||
entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
|
||||
assert.False(t, entitlements.HasLicense)
|
||||
})
|
||||
}
|
||||
@@ -20,8 +20,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
@@ -83,18 +81,7 @@ func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, str
|
||||
errC <- cmd.ExecuteContext(ctx)
|
||||
}()
|
||||
t.Cleanup(func() { require.NoError(t, <-errC) })
|
||||
|
||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
|
||||
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
dialer, err := client.DialWorkspaceAgentTailnet(ctx, slog.Logger{}, resources[0].Agents[0].ID)
|
||||
require.NoError(t, err)
|
||||
defer dialer.Close()
|
||||
require.Eventually(t, func() bool {
|
||||
_, err = dialer.Ping()
|
||||
return err == nil
|
||||
}, testutil.WaitMedium, testutil.IntervalFast)
|
||||
|
||||
return agentClient, agentToken, pubkey
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -91,12 +91,13 @@ func Core() []*cobra.Command {
|
||||
users(),
|
||||
versionCmd(),
|
||||
workspaceAgent(),
|
||||
features(),
|
||||
}
|
||||
}
|
||||
|
||||
func AGPL() []*cobra.Command {
|
||||
all := append(Core(), Server(coderd.New))
|
||||
all := append(Core(), Server(func(_ context.Context, o *coderd.Options) (*coderd.API, error) {
|
||||
return coderd.New(o), nil
|
||||
}))
|
||||
return all
|
||||
}
|
||||
|
||||
@@ -548,13 +549,11 @@ func checkWarnings(cmd *cobra.Command, client *codersdk.Client) error {
|
||||
defer cancel()
|
||||
|
||||
entitlements, err := client.Entitlements(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get entitlements to show warnings: %w", err)
|
||||
if err == nil {
|
||||
for _, w := range entitlements.Warnings {
|
||||
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Warn.Render(w))
|
||||
}
|
||||
}
|
||||
for _, w := range entitlements.Warnings {
|
||||
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Warn.Render(w))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -68,7 +68,7 @@ import (
|
||||
)
|
||||
|
||||
// nolint:gocyclo
|
||||
func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
|
||||
func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error)) *cobra.Command {
|
||||
var (
|
||||
accessURL string
|
||||
address string
|
||||
@@ -489,7 +489,10 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
|
||||
), promAddress, "prometheus")()
|
||||
}
|
||||
|
||||
coderAPI := newAPI(options)
|
||||
coderAPI, err := newAPI(ctx, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer coderAPI.Close()
|
||||
|
||||
client := codersdk.New(localURL)
|
||||
@@ -536,7 +539,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
|
||||
// These errors are typically noise like "TLS: EOF". Vault does similar:
|
||||
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
|
||||
ErrorLog: log.New(io.Discard, "", 0),
|
||||
Handler: coderAPI.Handler,
|
||||
Handler: coderAPI.RootHandler,
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return shutdownConnsCtx
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user