mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat(cli): extend duration to longer units (#15040)
This PR is a proposal to improve the situation described in #14750 For some precise commands - we would like to be able to use durations bigger than hours, minutes.. This PR extends the Duration proposed by Go with : - `d` - a day or 24hours. - `y` - a year or 365 days. I also removed the default value for lifetime and instead fetch the maxLifetime value from codersdk - so by default if no value set we use the value defined in the config.
This commit is contained in:
@ -6,7 +6,7 @@ USAGE:
|
|||||||
Create a token
|
Create a token
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
--lifetime duration, $CODER_TOKEN_LIFETIME (default: 720h0m0s)
|
--lifetime string, $CODER_TOKEN_LIFETIME
|
||||||
Specify a duration for the lifetime of the token.
|
Specify a duration for the lifetime of the token.
|
||||||
|
|
||||||
-n, --name string, $CODER_TOKEN_NAME
|
-n, --name string, $CODER_TOKEN_NAME
|
||||||
|
@ -46,7 +46,7 @@ func (r *RootCmd) tokens() *serpent.Command {
|
|||||||
|
|
||||||
func (r *RootCmd) createToken() *serpent.Command {
|
func (r *RootCmd) createToken() *serpent.Command {
|
||||||
var (
|
var (
|
||||||
tokenLifetime time.Duration
|
tokenLifetime string
|
||||||
name string
|
name string
|
||||||
user string
|
user string
|
||||||
)
|
)
|
||||||
@ -63,8 +63,30 @@ func (r *RootCmd) createToken() *serpent.Command {
|
|||||||
if user != "" {
|
if user != "" {
|
||||||
userID = user
|
userID = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var parsedLifetime time.Duration
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tokenConfig, err := client.GetTokenConfig(inv.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("get token config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenLifetime == "" {
|
||||||
|
parsedLifetime = tokenConfig.MaxTokenLifetime
|
||||||
|
} else {
|
||||||
|
parsedLifetime, err = extendedParseDuration(tokenLifetime)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("parse lifetime: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedLifetime > tokenConfig.MaxTokenLifetime {
|
||||||
|
return xerrors.Errorf("lifetime (%s) is greater than the maximum allowed lifetime (%s)", parsedLifetime, tokenConfig.MaxTokenLifetime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res, err := client.CreateToken(inv.Context(), userID, codersdk.CreateTokenRequest{
|
res, err := client.CreateToken(inv.Context(), userID, codersdk.CreateTokenRequest{
|
||||||
Lifetime: tokenLifetime,
|
Lifetime: parsedLifetime,
|
||||||
TokenName: name,
|
TokenName: name,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -82,8 +104,7 @@ func (r *RootCmd) createToken() *serpent.Command {
|
|||||||
Flag: "lifetime",
|
Flag: "lifetime",
|
||||||
Env: "CODER_TOKEN_LIFETIME",
|
Env: "CODER_TOKEN_LIFETIME",
|
||||||
Description: "Specify a duration for the lifetime of the token.",
|
Description: "Specify a duration for the lifetime of the token.",
|
||||||
Default: (time.Hour * 24 * 30).String(),
|
Value: serpent.StringOf(&tokenLifetime),
|
||||||
Value: serpent.DurationOf(&tokenLifetime),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Flag: "name",
|
Flag: "name",
|
||||||
|
73
cli/util.go
73
cli/util.go
@ -2,6 +2,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -181,6 +182,78 @@ func isDigit(s string) bool {
|
|||||||
}) == -1
|
}) == -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extendedParseDuration is a more lenient version of parseDuration that allows
|
||||||
|
// for more flexible input formats and cumulative durations.
|
||||||
|
// It allows for some extra units:
|
||||||
|
// - d (days, interpreted as 24h)
|
||||||
|
// - y (years, interpreted as 8_760h)
|
||||||
|
//
|
||||||
|
// FIXME: handle fractional values as discussed in https://github.com/coder/coder/pull/15040#discussion_r1799261736
|
||||||
|
func extendedParseDuration(raw string) (time.Duration, error) {
|
||||||
|
var d int64
|
||||||
|
isPositive := true
|
||||||
|
|
||||||
|
// handle negative durations by checking for a leading '-'
|
||||||
|
if strings.HasPrefix(raw, "-") {
|
||||||
|
raw = raw[1:]
|
||||||
|
isPositive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw == "" {
|
||||||
|
return 0, xerrors.Errorf("invalid duration: %q", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular expression to match any characters that do not match the expected duration format
|
||||||
|
invalidCharRe := regexp.MustCompile(`[^0-9|nsuµhdym]+`)
|
||||||
|
if invalidCharRe.MatchString(raw) {
|
||||||
|
return 0, xerrors.Errorf("invalid duration format: %q", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular expression to match numbers followed by 'd', 'y', or time units
|
||||||
|
re := regexp.MustCompile(`(-?\d+)(ns|us|µs|ms|s|m|h|d|y)`)
|
||||||
|
matches := re.FindAllStringSubmatch(raw, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
var num int64
|
||||||
|
num, err := strconv.ParseInt(match[1], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, xerrors.Errorf("invalid duration: %q", match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
switch match[2] {
|
||||||
|
case "d":
|
||||||
|
// we want to check if d + num * int64(24*time.Hour) would overflow
|
||||||
|
if d > (1<<63-1)-num*int64(24*time.Hour) {
|
||||||
|
return 0, xerrors.Errorf("invalid duration: %q", raw)
|
||||||
|
}
|
||||||
|
d += num * int64(24*time.Hour)
|
||||||
|
case "y":
|
||||||
|
// we want to check if d + num * int64(8760*time.Hour) would overflow
|
||||||
|
if d > (1<<63-1)-num*int64(8760*time.Hour) {
|
||||||
|
return 0, xerrors.Errorf("invalid duration: %q", raw)
|
||||||
|
}
|
||||||
|
d += num * int64(8760*time.Hour)
|
||||||
|
case "h", "m", "s", "ns", "us", "µs", "ms":
|
||||||
|
partDuration, err := time.ParseDuration(match[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, xerrors.Errorf("invalid duration: %q", match[0])
|
||||||
|
}
|
||||||
|
if d > (1<<63-1)-int64(partDuration) {
|
||||||
|
return 0, xerrors.Errorf("invalid duration: %q", raw)
|
||||||
|
}
|
||||||
|
d += int64(partDuration)
|
||||||
|
default:
|
||||||
|
return 0, xerrors.Errorf("invalid duration unit: %q", match[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isPositive {
|
||||||
|
return -time.Duration(d), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(d), nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseTime attempts to parse a time (no date) from the given string using a number of layouts.
|
// parseTime attempts to parse a time (no date) from the given string using a number of layouts.
|
||||||
func parseTime(s string) (time.Time, error) {
|
func parseTime(s string) (time.Time, error) {
|
||||||
// Try a number of possible layouts.
|
// Try a number of possible layouts.
|
||||||
|
@ -41,6 +41,50 @@ func TestDurationDisplay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtendedParseDuration(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
Duration string
|
||||||
|
Expected time.Duration
|
||||||
|
ExpectedOk bool
|
||||||
|
}{
|
||||||
|
{"1d", 24 * time.Hour, true},
|
||||||
|
{"1y", 365 * 24 * time.Hour, true},
|
||||||
|
{"10s", 10 * time.Second, true},
|
||||||
|
{"1m", 1 * time.Minute, true},
|
||||||
|
{"20h", 20 * time.Hour, true},
|
||||||
|
{"10y10d10s", 10*365*24*time.Hour + 10*24*time.Hour + 10*time.Second, true},
|
||||||
|
{"10ms", 10 * time.Millisecond, true},
|
||||||
|
{"5y10d10s5y2ms8ms", 10*365*24*time.Hour + 10*24*time.Hour + 10*time.Second + 10*time.Millisecond, true},
|
||||||
|
{"10yz10d10s", 0, false},
|
||||||
|
{"1µs2h1d", 1*time.Microsecond + 2*time.Hour + 1*24*time.Hour, true},
|
||||||
|
{"1y365d", 2 * 365 * 24 * time.Hour, true},
|
||||||
|
{"1µs10us", 1*time.Microsecond + 10*time.Microsecond, true},
|
||||||
|
// negative related tests
|
||||||
|
{"-", 0, false},
|
||||||
|
{"-2h10m", -2*time.Hour - 10*time.Minute, true},
|
||||||
|
{"--10s", 0, false},
|
||||||
|
{"10s-10m", 0, false},
|
||||||
|
// overflow related tests
|
||||||
|
{"-20000000000000h", 0, false},
|
||||||
|
{"92233754775807y", 0, false},
|
||||||
|
{"200y200y200y200y200y", 0, false},
|
||||||
|
{"9223372036854775807s", 0, false},
|
||||||
|
} {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(testCase.Duration, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
actual, err := extendedParseDuration(testCase.Duration)
|
||||||
|
if testCase.ExpectedOk {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, testCase.Expected, actual)
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRelative(t *testing.T) {
|
func TestRelative(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
assert.Equal(t, relative(time.Minute), "in 1m")
|
assert.Equal(t, relative(time.Minute), "in 1m")
|
||||||
|
3
docs/reference/cli/tokens_create.md
generated
3
docs/reference/cli/tokens_create.md
generated
@ -16,9 +16,8 @@ coder tokens create [flags]
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
| ----------- | ---------------------------------- |
|
| ----------- | ---------------------------------- |
|
||||||
| Type | <code>duration</code> |
|
| Type | <code>string</code> |
|
||||||
| Environment | <code>$CODER_TOKEN_LIFETIME</code> |
|
| Environment | <code>$CODER_TOKEN_LIFETIME</code> |
|
||||||
| Default | <code>720h0m0s</code> |
|
|
||||||
|
|
||||||
Specify a duration for the lifetime of the token.
|
Specify a duration for the lifetime of the token.
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user