mirror of
https://github.com/kubernetes/client-go.git
synced 2025-06-26 07:02:01 +00:00
Merge pull request #91305 from ankeesler/ankeesler/exec-cred-prov-install-hint
exec credential provider: add install hint Kubernetes-commit: 422e4421f1d4063ff5a0c394b7f54eb23374fa5f
This commit is contained in:
commit
b9931a5691
2
Godeps/Godeps.json
generated
2
Godeps/Godeps.json
generated
@ -440,7 +440,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/api",
|
||||
"Rev": "16d572fe0f9f"
|
||||
"Rev": "6943fd7f818f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery",
|
||||
|
4
go.mod
4
go.mod
@ -26,7 +26,7 @@ require (
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
k8s.io/api v0.0.0-20200702130436-16d572fe0f9f
|
||||
k8s.io/api v0.0.0-20200706140252-6943fd7f818f
|
||||
k8s.io/apimachinery v0.0.0-20200702090251-3c2682fedbf2
|
||||
k8s.io/klog/v2 v2.2.0
|
||||
k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19
|
||||
@ -34,6 +34,6 @@ require (
|
||||
)
|
||||
|
||||
replace (
|
||||
k8s.io/api => k8s.io/api v0.0.0-20200702130436-16d572fe0f9f
|
||||
k8s.io/api => k8s.io/api v0.0.0-20200706140252-6943fd7f818f
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20200702090251-3c2682fedbf2
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@ -332,7 +332,7 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
k8s.io/api v0.0.0-20200702130436-16d572fe0f9f/go.mod h1:PFJ2b4cud2iXU+DzN8Tf+HsJsN2KKcJUfbMmckp2bhI=
|
||||
k8s.io/api v0.0.0-20200706140252-6943fd7f818f/go.mod h1:PFJ2b4cud2iXU+DzN8Tf+HsJsN2KKcJUfbMmckp2bhI=
|
||||
k8s.io/apimachinery v0.0.0-20200702090251-3c2682fedbf2/go.mod h1:87+lSRzC+mGLZTTwIcPY67sorIVlqPhdNrvsToaUUns=
|
||||
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog/v2 v2.0.0 h1:Foj74zO6RbjjP4hBEKjnYtjjAhGg4jNynUdYF6fJrok=
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -38,6 +39,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
|
||||
@ -51,6 +53,12 @@ import (
|
||||
|
||||
const execInfoEnv = "KUBERNETES_EXEC_INFO"
|
||||
const onRotateListWarningLength = 1000
|
||||
const installHintVerboseHelp = `
|
||||
|
||||
It looks like you are trying to use a client-go credential plugin that is not installed.
|
||||
|
||||
To learn more about this feature, consult the documentation available at:
|
||||
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins`
|
||||
|
||||
var scheme = runtime.NewScheme()
|
||||
var codecs = serializer.NewCodecFactory(scheme)
|
||||
@ -108,6 +116,44 @@ func (c *cache) put(s string, a *Authenticator) *Authenticator {
|
||||
return a
|
||||
}
|
||||
|
||||
// sometimes rate limits how often a function f() is called. Specifically, Do()
|
||||
// will run the provided function f() up to threshold times every interval
|
||||
// duration.
|
||||
type sometimes struct {
|
||||
threshold int
|
||||
interval time.Duration
|
||||
|
||||
clock clock.Clock
|
||||
mu sync.Mutex
|
||||
|
||||
count int // times we have called f() in this window
|
||||
window time.Time // beginning of current window of length interval
|
||||
}
|
||||
|
||||
func (s *sometimes) Do(f func()) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := s.clock.Now()
|
||||
if s.window.IsZero() {
|
||||
s.window = now
|
||||
}
|
||||
|
||||
// If we are no longer in our saved time window, then we get to reset our run
|
||||
// count back to 0 and start increasing towards the threshold again.
|
||||
if inWindow := now.Sub(s.window) < s.interval; !inWindow {
|
||||
s.window = now
|
||||
s.count = 0
|
||||
}
|
||||
|
||||
// If we have not run the function more than threshold times in this current
|
||||
// time window, we get to run it now!
|
||||
if underThreshold := s.count < s.threshold; underThreshold {
|
||||
s.count++
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthenticator returns an exec-based plugin for providing client credentials.
|
||||
func GetAuthenticator(config *api.ExecConfig) (*Authenticator, error) {
|
||||
return newAuthenticator(globalCache, config)
|
||||
@ -129,6 +175,13 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error)
|
||||
args: config.Args,
|
||||
group: gv,
|
||||
|
||||
installHint: config.InstallHint,
|
||||
sometimes: &sometimes{
|
||||
threshold: 10,
|
||||
interval: time.Hour,
|
||||
clock: clock.RealClock{},
|
||||
},
|
||||
|
||||
stdin: os.Stdin,
|
||||
stderr: os.Stderr,
|
||||
interactive: terminal.IsTerminal(int(os.Stdout.Fd())),
|
||||
@ -152,6 +205,12 @@ type Authenticator struct {
|
||||
group schema.GroupVersion
|
||||
env []string
|
||||
|
||||
// Used to avoid log spew by rate limiting install hint printing. We didn't do
|
||||
// this by interval based rate limiting alone since that way may have prevented
|
||||
// the install hint from showing up for kubectl users.
|
||||
sometimes *sometimes
|
||||
installHint string
|
||||
|
||||
// Stubbable for testing
|
||||
stdin io.Reader
|
||||
stderr io.Writer
|
||||
@ -323,7 +382,7 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("exec: %v", err)
|
||||
return a.wrapCmdRunErrorLocked(err)
|
||||
}
|
||||
|
||||
_, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred)
|
||||
@ -394,3 +453,35 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
expirationMetrics.set(a, expiry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapCmdRunErrorLocked pulls out the code to construct a helpful error message
|
||||
// for when the exec plugin's binary fails to Run().
|
||||
//
|
||||
// It must be called while holding the Authenticator's mutex.
|
||||
func (a *Authenticator) wrapCmdRunErrorLocked(err error) error {
|
||||
switch err.(type) {
|
||||
case *exec.Error: // Binary does not exist (see exec.Error).
|
||||
builder := strings.Builder{}
|
||||
fmt.Fprintf(&builder, "exec: executable %s not found", a.cmd)
|
||||
|
||||
a.sometimes.Do(func() {
|
||||
fmt.Fprint(&builder, installHintVerboseHelp)
|
||||
if a.installHint != "" {
|
||||
fmt.Fprintf(&builder, "\n\n%s", a.installHint)
|
||||
}
|
||||
})
|
||||
|
||||
return errors.New(builder.String())
|
||||
|
||||
case *exec.ExitError: // Binary execution failed (see exec.Cmd.Run()).
|
||||
e := err.(*exec.ExitError)
|
||||
return fmt.Errorf(
|
||||
"exec: executable %s failed with exit code %d",
|
||||
a.cmd,
|
||||
e.ProcessState.ExitCode(),
|
||||
)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("exec: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -32,12 +32,14 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
"k8s.io/client-go/transport"
|
||||
@ -168,15 +170,17 @@ func compJSON(t *testing.T, got, want []byte) {
|
||||
|
||||
func TestRefreshCreds(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config api.ExecConfig
|
||||
output string
|
||||
interactive bool
|
||||
response *clientauthentication.Response
|
||||
wantInput string
|
||||
wantCreds credentials
|
||||
wantExpiry time.Time
|
||||
wantErr bool
|
||||
name string
|
||||
config api.ExecConfig
|
||||
exitCode int
|
||||
output string
|
||||
interactive bool
|
||||
response *clientauthentication.Response
|
||||
wantInput string
|
||||
wantCreds credentials
|
||||
wantExpiry time.Time
|
||||
wantErr bool
|
||||
wantErrSubstr string
|
||||
}{
|
||||
{
|
||||
name: "basic-request",
|
||||
@ -450,17 +454,42 @@ func TestRefreshCreds(t *testing.T) {
|
||||
}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown-binary",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Command: "does not exist",
|
||||
InstallHint: "some install hint",
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrSubstr: "some install hint",
|
||||
},
|
||||
{
|
||||
name: "binary-fails",
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
exitCode: 73,
|
||||
wantErr: true,
|
||||
wantErrSubstr: "73",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := test.config
|
||||
|
||||
c.Command = "./testdata/test-plugin.sh"
|
||||
c.Env = append(c.Env, api.ExecEnvVar{
|
||||
Name: "TEST_OUTPUT",
|
||||
Value: test.output,
|
||||
})
|
||||
if c.Command == "" {
|
||||
c.Command = "./testdata/test-plugin.sh"
|
||||
c.Env = append(c.Env, api.ExecEnvVar{
|
||||
Name: "TEST_OUTPUT",
|
||||
Value: test.output,
|
||||
})
|
||||
c.Env = append(c.Env, api.ExecEnvVar{
|
||||
Name: "TEST_EXIT_CODE",
|
||||
Value: strconv.Itoa(test.exitCode),
|
||||
})
|
||||
}
|
||||
|
||||
a, err := newAuthenticator(newCache(), &c)
|
||||
if err != nil {
|
||||
@ -475,6 +504,8 @@ func TestRefreshCreds(t *testing.T) {
|
||||
if err := a.refreshCredsLocked(test.response); err != nil {
|
||||
if !test.wantErr {
|
||||
t.Errorf("get token %v", err)
|
||||
} else if !strings.Contains(err.Error(), test.wantErrSubstr) {
|
||||
t.Errorf("expected error with substring '%v' got '%v'", test.wantErrSubstr, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -763,6 +794,75 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) {
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
func TestInstallHintRateLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
threshold int
|
||||
interval time.Duration
|
||||
|
||||
calls int
|
||||
perCallAdvance time.Duration
|
||||
|
||||
wantInstallHint int
|
||||
}{
|
||||
{
|
||||
name: "print-up-to-threshold",
|
||||
threshold: 2,
|
||||
interval: time.Second,
|
||||
calls: 10,
|
||||
wantInstallHint: 2,
|
||||
},
|
||||
{
|
||||
name: "after-interval-threshold-resets",
|
||||
threshold: 2,
|
||||
interval: time.Second * 5,
|
||||
calls: 10,
|
||||
perCallAdvance: time.Second,
|
||||
wantInstallHint: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := api.ExecConfig{
|
||||
Command: "does not exist",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
InstallHint: "some install hint",
|
||||
}
|
||||
a, err := newAuthenticator(newCache(), &c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a.sometimes.threshold = test.threshold
|
||||
a.sometimes.interval = test.interval
|
||||
|
||||
clock := clock.NewFakeClock(time.Now())
|
||||
a.sometimes.clock = clock
|
||||
|
||||
count := 0
|
||||
for i := 0; i < test.calls; i++ {
|
||||
err := a.refreshCredsLocked(&clientauthentication.Response{})
|
||||
if strings.Contains(err.Error(), c.InstallHint) {
|
||||
count++
|
||||
}
|
||||
|
||||
clock.SetTime(clock.Now().Add(test.perCallAdvance))
|
||||
}
|
||||
|
||||
if test.wantInstallHint != count {
|
||||
t.Errorf(
|
||||
"%s: expected install hint %d times got %d",
|
||||
test.name,
|
||||
test.wantInstallHint,
|
||||
count,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// genClientCert generates an x509 certificate for testing. Certificate and key
|
||||
// are returned in PEM encoding. The generated cert expires in 24 hours.
|
||||
func genClientCert(t *testing.T) ([]byte, []byte) {
|
||||
|
@ -16,3 +16,4 @@
|
||||
|
||||
>&2 echo "$KUBERNETES_EXEC_INFO"
|
||||
echo "$TEST_OUTPUT"
|
||||
exit "${TEST_EXIT_CODE:-0}"
|
||||
|
@ -193,7 +193,7 @@ func (c AuthProviderConfig) String() string {
|
||||
// ExecConfig specifies a command to provide client credentials. The command is exec'd
|
||||
// and outputs structured stdout holding credentials.
|
||||
//
|
||||
// See the client.authentiction.k8s.io API group for specifications of the exact input
|
||||
// See the client.authentication.k8s.io API group for specifications of the exact input
|
||||
// and output format
|
||||
type ExecConfig struct {
|
||||
// Command to execute.
|
||||
@ -210,6 +210,11 @@ type ExecConfig struct {
|
||||
// Preferred input version of the ExecInfo. The returned ExecCredentials MUST use
|
||||
// the same encoding version as the input.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
|
||||
// This text is shown to the user when the executable doesn't seem to be
|
||||
// present. For example, `brew install foo-cli` might be a good InstallHint for
|
||||
// foo-cli on Mac OS systems.
|
||||
InstallHint string `json:"installHint,omitempty"`
|
||||
}
|
||||
|
||||
var _ fmt.Stringer = new(ExecConfig)
|
||||
|
@ -192,7 +192,7 @@ type AuthProviderConfig struct {
|
||||
// ExecConfig specifies a command to provide client credentials. The command is exec'd
|
||||
// and outputs structured stdout holding credentials.
|
||||
//
|
||||
// See the client.authentiction.k8s.io API group for specifications of the exact input
|
||||
// See the client.authentication.k8s.io API group for specifications of the exact input
|
||||
// and output format
|
||||
type ExecConfig struct {
|
||||
// Command to execute.
|
||||
@ -209,6 +209,11 @@ type ExecConfig struct {
|
||||
// Preferred input version of the ExecInfo. The returned ExecCredentials MUST use
|
||||
// the same encoding version as the input.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
|
||||
// This text is shown to the user when the executable doesn't seem to be
|
||||
// present. For example, `brew install foo-cli` might be a good InstallHint for
|
||||
// foo-cli on Mac OS systems.
|
||||
InstallHint string `json:"installHint,omitempty"`
|
||||
}
|
||||
|
||||
// ExecEnvVar is used for setting environment variables when executing an exec-based
|
||||
|
@ -358,6 +358,7 @@ func autoConvert_v1_ExecConfig_To_api_ExecConfig(in *ExecConfig, out *api.ExecCo
|
||||
out.Args = *(*[]string)(unsafe.Pointer(&in.Args))
|
||||
out.Env = *(*[]api.ExecEnvVar)(unsafe.Pointer(&in.Env))
|
||||
out.APIVersion = in.APIVersion
|
||||
out.InstallHint = in.InstallHint
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -371,6 +372,7 @@ func autoConvert_api_ExecConfig_To_v1_ExecConfig(in *api.ExecConfig, out *ExecCo
|
||||
out.Args = *(*[]string)(unsafe.Pointer(&in.Args))
|
||||
out.Env = *(*[]ExecEnvVar)(unsafe.Pointer(&in.Env))
|
||||
out.APIVersion = in.APIVersion
|
||||
out.InstallHint = in.InstallHint
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
restclient "k8s.io/client-go/rest"
|
||||
clientauth "k8s.io/client-go/tools/auth"
|
||||
@ -269,6 +270,7 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI
|
||||
}
|
||||
if configAuthInfo.Exec != nil {
|
||||
mergedConfig.ExecProvider = configAuthInfo.Exec
|
||||
mergedConfig.ExecProvider.InstallHint = cleanANSIEscapeCodes(mergedConfig.ExecProvider.InstallHint)
|
||||
}
|
||||
|
||||
// if there still isn't enough information to authenticate the user, try prompting
|
||||
@ -314,6 +316,41 @@ func canIdentifyUser(config restclient.Config) bool {
|
||||
config.ExecProvider != nil
|
||||
}
|
||||
|
||||
// cleanANSIEscapeCodes takes an arbitrary string and ensures that there are no
|
||||
// ANSI escape sequences that could put the terminal in a weird state (e.g.,
|
||||
// "\e[1m" bolds text)
|
||||
func cleanANSIEscapeCodes(s string) string {
|
||||
// spaceControlCharacters includes tab, new line, vertical tab, new page, and
|
||||
// carriage return. These are in the unicode.Cc category, but that category also
|
||||
// contains ESC (U+001B) which we don't want.
|
||||
spaceControlCharacters := unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{Lo: 0x0009, Hi: 0x000D, Stride: 1},
|
||||
},
|
||||
}
|
||||
|
||||
// Why not make this deny-only (instead of allow-only)? Because unicode.C
|
||||
// contains newline and tab characters that we want.
|
||||
allowedRanges := []*unicode.RangeTable{
|
||||
unicode.L,
|
||||
unicode.M,
|
||||
unicode.N,
|
||||
unicode.P,
|
||||
unicode.S,
|
||||
unicode.Z,
|
||||
&spaceControlCharacters,
|
||||
}
|
||||
builder := strings.Builder{}
|
||||
for _, roon := range s {
|
||||
if unicode.IsOneOf(allowedRanges, roon) {
|
||||
builder.WriteRune(roon) // returns nil error, per go doc
|
||||
} else {
|
||||
fmt.Fprintf(&builder, "%U", roon)
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// Namespace implements ClientConfig
|
||||
func (config *DirectClientConfig) Namespace() (string, bool, error) {
|
||||
if config.overrides != nil && config.overrides.Context.Namespace != "" {
|
||||
|
@ -624,6 +624,26 @@ func TestCreateMissingContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAuthConfigExecInstallHintCleanup(t *testing.T) {
|
||||
config := createValidTestConfig()
|
||||
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{
|
||||
AuthInfo: clientcmdapi.AuthInfo{
|
||||
Exec: &clientcmdapi.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
Command: "some-command",
|
||||
InstallHint: "some install hint with \x1b[1mcontrol chars\x1b[0m\nand a newline",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
cleanedInstallHint := "some install hint with U+001B[1mcontrol charsU+001B[0m\nand a newline"
|
||||
|
||||
clientConfig, err := clientBuilder.ClientConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
matchStringArg(cleanedInstallHint, clientConfig.ExecProvider.InstallHint, t)
|
||||
}
|
||||
|
||||
func TestInClusterClientConfigPrecedence(t *testing.T) {
|
||||
tt := []struct {
|
||||
overrides *ConfigOverrides
|
||||
@ -850,3 +870,70 @@ users:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCleanANSIEscapeCodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in, out string
|
||||
}{
|
||||
{
|
||||
name: "DenyBoldCharacters",
|
||||
in: "\x1b[1mbold tuna\x1b[0m, fish, \x1b[1mbold marlin\x1b[0m",
|
||||
out: "U+001B[1mbold tunaU+001B[0m, fish, U+001B[1mbold marlinU+001B[0m",
|
||||
},
|
||||
{
|
||||
name: "DenyCursorNavigation",
|
||||
in: "\x1b[2Aup up, \x1b[2Cright right",
|
||||
out: "U+001B[2Aup up, U+001B[2Cright right",
|
||||
},
|
||||
{
|
||||
name: "DenyClearScreen",
|
||||
in: "clear: \x1b[2J",
|
||||
out: "clear: U+001B[2J",
|
||||
},
|
||||
{
|
||||
name: "AllowSpaceCharactersUnchanged",
|
||||
in: "tuna\nfish\r\nmarlin\t\r\ntuna\vfish\fmarlin",
|
||||
},
|
||||
{
|
||||
name: "AllowLetters",
|
||||
in: "alpha: \u03b1, beta: \u03b2, gamma: \u03b3",
|
||||
},
|
||||
{
|
||||
name: "AllowMarks",
|
||||
in: "tu\u0301na with a mark over the u, fi\u0302sh with a mark over the i," +
|
||||
" ma\u030Arlin with a mark over the a",
|
||||
},
|
||||
{
|
||||
name: "AllowNumbers",
|
||||
in: "t1na, f2sh, m3rlin, t12a, f34h, m56lin, t123, f456, m567n",
|
||||
},
|
||||
{
|
||||
name: "AllowPunctuation",
|
||||
in: "\"here's a sentence; with! some...punctuation ;)\"",
|
||||
},
|
||||
{
|
||||
name: "AllowSymbols",
|
||||
in: "the integral of f(x) from 0 to n approximately equals the sum of f(x)" +
|
||||
" from a = 0 to n, where a and n are natural numbers:" +
|
||||
"\u222b\u2081\u207F f(x) dx \u2248 \u2211\u2090\u208C\u2081\u207F f(x)," +
|
||||
" a \u2208 \u2115, n \u2208 \u2115",
|
||||
},
|
||||
{
|
||||
name: "AllowSepatators",
|
||||
in: "here is a paragraph separator\u2029and here\u2003are\u2003some" +
|
||||
"\u2003em\u2003spaces",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if len(test.out) == 0 {
|
||||
test.out = test.in
|
||||
}
|
||||
|
||||
if actualOut := cleanANSIEscapeCodes(test.in); test.out != actualOut {
|
||||
t.Errorf("expected %q, actual %q", test.out, actualOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user