github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/common.go (about)

     1  /*
     2   * Copyright 2018-2022 The NATS Authors
     3   * Licensed under the Apache License, Version 2.0 (the "License");
     4   * you may not use this file except in compliance with the License.
     5   * You may obtain a copy of the License at
     6   *
     7   * http://www.apache.org/licenses/LICENSE-2.0
     8   *
     9   * Unless required by applicable law or agreed to in writing, software
    10   * distributed under the License is distributed on an "AS IS" BASIS,
    11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12   * See the License for the specific language governing permissions and
    13   * limitations under the License.
    14   */
    15  
    16  package cmd
    17  
    18  import (
    19  	"bytes"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"path"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/dustin/go-humanize"
    34  	"github.com/mitchellh/go-homedir"
    35  	cli "github.com/nats-io/cliprompts/v2"
    36  	"github.com/nats-io/jwt/v2"
    37  	"github.com/nats-io/nkeys"
    38  	"github.com/spf13/cobra"
    39  	"golang.org/x/text/cases"
    40  	"golang.org/x/text/language"
    41  
    42  	"github.com/nats-io/nsc/v2/cmd/store"
    43  )
    44  
    45  // ResolvePath resolves a directory/file from an environment variable
    46  // if not set defaultPath is returned
    47  func ResolvePath(defaultPath string, varName string) string {
    48  	v := os.Getenv(varName)
    49  	if v != "" {
    50  		return v
    51  	}
    52  	return defaultPath
    53  }
    54  
    55  func GetOutput(fp string) (*os.File, error) {
    56  	var f *os.File
    57  
    58  	if fp == "--" {
    59  		f = os.Stdout
    60  	} else {
    61  		afp, err := filepath.Abs(fp)
    62  		if err != nil {
    63  			return nil, fmt.Errorf("error calculating abs %#q: %v", fp, err)
    64  		}
    65  		_, err = os.Stat(afp)
    66  		if err == nil {
    67  			return nil, fmt.Errorf("%#q already exists", afp)
    68  		}
    69  		if !os.IsNotExist(err) {
    70  			return nil, err
    71  		}
    72  
    73  		f, err = os.Create(afp)
    74  		if err != nil {
    75  			return nil, fmt.Errorf("error creating output file %#q: %v", afp, err)
    76  		}
    77  	}
    78  	return f, nil
    79  }
    80  
    81  func IsStdOut(fp string) bool {
    82  	return fp == "--"
    83  }
    84  
    85  func WriteJson(fp string, v interface{}) error {
    86  	data, err := json.Marshal(v)
    87  	if err != nil {
    88  		return fmt.Errorf("error marshaling: %v", err)
    89  	}
    90  
    91  	parent := filepath.Dir(fp)
    92  	if parent != "" {
    93  		if err := os.MkdirAll(parent, 0700); err != nil {
    94  			return fmt.Errorf("error creating dirs %#q: %v", fp, err)
    95  		}
    96  	}
    97  	if err := os.WriteFile(fp, data, 0600); err != nil {
    98  		return fmt.Errorf("error writing %#q: %v", fp, err)
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  func Write(fp string, data []byte) error {
   105  	var err error
   106  	var f *os.File
   107  
   108  	f, err = GetOutput(fp)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	if !IsStdOut(fp) {
   113  		defer f.Close()
   114  	}
   115  	_, err = f.Write(data)
   116  	if err != nil {
   117  		return fmt.Errorf("error writing %#q: %v", fp, err)
   118  	}
   119  
   120  	if !IsStdOut(fp) {
   121  		if err := f.Sync(); err != nil {
   122  			return err
   123  		}
   124  	}
   125  	return nil
   126  }
   127  
   128  func ReadJson(fp string, v interface{}) error {
   129  	data, err := Read(fp)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	err = json.Unmarshal(data, &v)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	return nil
   138  }
   139  
   140  func Read(fp string) ([]byte, error) {
   141  	fp, err := Expand(fp)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	return os.ReadFile(fp)
   146  }
   147  
   148  func ParseNumber(s string) (int64, error) {
   149  	if s == "" {
   150  		return 0, nil
   151  	}
   152  	isNeg := strings.HasPrefix(s, "-")
   153  	if isNeg {
   154  		s = strings.TrimPrefix(s, "-")
   155  	}
   156  	i, err := humanize.ParseBytes(s)
   157  	if err != nil {
   158  		return 0, err
   159  	}
   160  	if isNeg {
   161  		return -(int64)(i), nil
   162  	}
   163  	return (int64)(i), nil
   164  }
   165  
   166  func UnixToDate(d int64) string {
   167  	if d == 0 {
   168  		return ""
   169  	}
   170  
   171  	return strings.Replace(time.Unix(d, 0).UTC().String(), " +0000", "", -1)
   172  }
   173  
   174  func HumanizedDate(d int64) string {
   175  	if d == 0 {
   176  		return ""
   177  	}
   178  	now := time.Now()
   179  	when := time.Unix(d, 0).UTC()
   180  
   181  	if now.After(when) {
   182  		return strings.TrimSpace(TitleCase(humanize.RelTime(when, now, "ago", "")))
   183  	} else {
   184  		return strings.TrimSpace(TitleCase("in " + humanize.RelTime(now, when, "", "")))
   185  	}
   186  }
   187  
   188  func RenderDate(d int64) string {
   189  	if d == 0 {
   190  		return ""
   191  	}
   192  
   193  	return UnixToDate(d)
   194  }
   195  
   196  func NKeyValidator(kind nkeys.PrefixByte) cli.Validator {
   197  	return func(v string) error {
   198  		if v == "" {
   199  			return fmt.Errorf("value cannot be empty")
   200  		}
   201  		nk, err := store.ResolveKey(v)
   202  		if err != nil {
   203  			return err
   204  		}
   205  		if nk == nil {
   206  			// if it looks like a file, provide a better message
   207  			if strings.Contains(v, string(os.PathSeparator)) {
   208  				_, err := os.Stat(v)
   209  				if err != nil {
   210  					return err
   211  				}
   212  			}
   213  			return fmt.Errorf("%q is not a valid nkey", v)
   214  		}
   215  		t, err := store.KeyType(nk)
   216  		if err != nil {
   217  			return err
   218  		}
   219  		if t != kind {
   220  			return fmt.Errorf("specified key is not valid for an %s", kind.String())
   221  		}
   222  		return nil
   223  	}
   224  }
   225  
   226  func SeedNKeyValidatorMatching(pukeys []string, kinds ...nkeys.PrefixByte) cli.Validator {
   227  	return func(v string) error {
   228  		if v == "" {
   229  			return fmt.Errorf("value cannot be empty")
   230  		}
   231  		nk, err := store.ResolveKey(v)
   232  		if err != nil {
   233  			return err
   234  		}
   235  		if nk == nil {
   236  			// if it looks like a file, provide a better message
   237  			if strings.Contains(v, string(os.PathSeparator)) {
   238  				_, err := os.Stat(v)
   239  				if err != nil {
   240  					return err
   241  				}
   242  			}
   243  			return fmt.Errorf("%q is not a valid nkey", v)
   244  		}
   245  		t, err := store.KeyType(nk)
   246  		if err != nil {
   247  			return err
   248  		}
   249  		foundKind := false
   250  		kindNames := []string{}
   251  		for _, kind := range kinds {
   252  			if t == kind {
   253  				foundKind = true
   254  				break
   255  			}
   256  			kindNames = append(kindNames, kind.String())
   257  		}
   258  		if !foundKind {
   259  			return fmt.Errorf("specified key is not valid for any of %v", kindNames)
   260  		}
   261  
   262  		pk, err := nk.PublicKey()
   263  		if err != nil {
   264  			return fmt.Errorf("error extracting public key: %v", err)
   265  		}
   266  
   267  		found := false
   268  		for _, k := range pukeys {
   269  			if k == pk {
   270  				found = true
   271  				break
   272  			}
   273  		}
   274  		if !found {
   275  			return fmt.Errorf("%q is not an expected signing key %v", v, pukeys)
   276  		}
   277  
   278  		_, err = nk.Seed()
   279  		if err != nil {
   280  			return err
   281  		}
   282  
   283  		return nil
   284  	}
   285  }
   286  
   287  func IsURL(v string) bool {
   288  	if u, err := url.Parse(v); err == nil {
   289  		s := strings.ToLower(u.Scheme)
   290  		return s == "http" || s == "https"
   291  	}
   292  	return false
   293  }
   294  
   295  func LoadFromFileOrURL(v string) ([]byte, error) {
   296  	// we expect either a file or url
   297  	if IsURL(v) {
   298  		return LoadFromURL(v)
   299  	}
   300  	v, err := Expand(v)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	_, err = os.Stat(v)
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  	return Read(v)
   309  }
   310  
   311  func LoadFromURL(url string) ([]byte, error) {
   312  	c := &http.Client{Timeout: time.Second * 5}
   313  	r, err := c.Get(url)
   314  	if err != nil {
   315  		return nil, fmt.Errorf("error loading %q: %v", url, err)
   316  	}
   317  	defer r.Body.Close()
   318  	if r.StatusCode != http.StatusOK {
   319  		return nil, fmt.Errorf("error reading response from %q: %v", url, r.Status)
   320  	}
   321  	var buf bytes.Buffer
   322  	_, err = io.Copy(&buf, r.Body)
   323  	if err != nil {
   324  		return nil, fmt.Errorf("error reading response from %q: %v", url, err)
   325  	}
   326  	data := buf.Bytes()
   327  	return data, nil
   328  }
   329  
   330  func IsValidDir(dir string) error {
   331  	fi, err := os.Stat(dir)
   332  	if err != nil {
   333  		return err
   334  	}
   335  	if !fi.IsDir() {
   336  		return fmt.Errorf("not a directory")
   337  	}
   338  	return nil
   339  }
   340  
   341  func MaybeMakeDir(dir string) error {
   342  	fi, err := os.Stat(dir)
   343  	if err != nil && os.IsNotExist(err) {
   344  		if err := os.MkdirAll(dir, 0700); err != nil {
   345  			return fmt.Errorf("error creating %#q: %v", dir, err)
   346  		}
   347  	} else if err != nil {
   348  		return fmt.Errorf("error stat'ing %#q: %v", dir, err)
   349  	} else if !fi.IsDir() {
   350  		return fmt.Errorf("%#q already exists and it is not a dir", dir)
   351  	}
   352  	return nil
   353  }
   354  
   355  func AbbrevHomePaths(fp string) string {
   356  	h, err := homedir.Dir()
   357  	if err != nil {
   358  		return fp
   359  	}
   360  	if strings.HasPrefix(fp, h) {
   361  		return strings.Replace(fp, h, "~", 1)
   362  	}
   363  	return fp
   364  }
   365  
   366  func NameFlagOrArgument(name string, ctx ActionCtx) string {
   367  	return nameFlagOrArgument(name, ctx.Args())
   368  }
   369  
   370  func nameFlagOrArgument(name string, args []string) string {
   371  	if name == "" && len(args) > 0 {
   372  		return args[0]
   373  	}
   374  	return name
   375  }
   376  
   377  func MaxArgs(max int) cobra.PositionalArgs {
   378  	// if we are running in a test, remove the limit
   379  	if strings.Contains(strings.Join(os.Args, " "), "-test.v") {
   380  		return nil
   381  	}
   382  	return cobra.MaximumNArgs(max)
   383  }
   384  
   385  // ExpandPath expands the specified path calls. Resolves ~/ and ./.. paths.
   386  func Expand(s string) (string, error) {
   387  	var err error
   388  	s, err = homedir.Expand(s)
   389  	if err != nil {
   390  		return "", err
   391  	}
   392  	return filepath.Abs(s)
   393  }
   394  
   395  func PushAccount(u string, accountjwt []byte) (int, []byte, error) {
   396  	resp, err := http.Post(u, "application/text", bytes.NewReader(accountjwt))
   397  	if err != nil {
   398  		return 0, nil, err
   399  	}
   400  	defer resp.Body.Close()
   401  	data, err := io.ReadAll(resp.Body)
   402  	return resp.StatusCode, data, err
   403  }
   404  
   405  func IsAccountAvailable(status int) bool {
   406  	return status == http.StatusOK
   407  }
   408  
   409  func IsAccountPending(status int) bool {
   410  	return status > http.StatusOK && status < 300
   411  }
   412  
   413  // Validate an operator name
   414  func OperatorNameValidator(v string) error {
   415  	operators := GetConfig().ListOperators()
   416  	for _, o := range operators {
   417  		if o == v {
   418  			r := GetConfig().StoreRoot
   419  			return fmt.Errorf("an operator named %#q already exists in %#q - specify a different directory with --dir", v, r)
   420  		}
   421  	}
   422  	return nil
   423  }
   424  
   425  func AccountJwtURLFromString(asu string, accountSubject string) (string, error) {
   426  	u, err := url.Parse(asu)
   427  	if err != nil {
   428  		return "", err
   429  	}
   430  	u.Path = path.Join(u.Path, "accounts", accountSubject)
   431  	return u.String(), nil
   432  }
   433  
   434  func AccountJwtURL(oc *jwt.OperatorClaims, ac *jwt.AccountClaims) (string, error) {
   435  	if oc.AccountServerURL == "" {
   436  		return "", fmt.Errorf("error: operator %q doesn't set an account server url", oc.Name)
   437  	}
   438  	return AccountJwtURLFromString(oc.AccountServerURL, ac.Subject)
   439  }
   440  
   441  func OperatorJwtURLFromString(asu string) (string, error) {
   442  	u, err := url.Parse(asu)
   443  	if err != nil {
   444  		return "", err
   445  	}
   446  	u.Path = path.Join(u.Path, "operator")
   447  	return u.String(), nil
   448  }
   449  
   450  func OperatorJwtURL(oc *jwt.OperatorClaims) (string, error) {
   451  	if oc.AccountServerURL == "" {
   452  		return "", fmt.Errorf("error: operator %q doesn't set an account server url", oc.Name)
   453  	}
   454  	return OperatorJwtURLFromString(oc.AccountServerURL)
   455  }
   456  
   457  func IsNatsUrl(url string) bool {
   458  	return store.IsNatsUrl(url)
   459  }
   460  
   461  func IsAccountServerURL(u string) bool {
   462  	return store.IsAccountServerURL(u)
   463  }
   464  
   465  func IsResolverURL(u string) bool {
   466  	return store.IsResolverURL(u)
   467  }
   468  
   469  func ValidSigner(kp nkeys.KeyPair, signers []string) (bool, error) {
   470  	pk, err := kp.PublicKey()
   471  	if err != nil {
   472  		return false, err
   473  	}
   474  	ok := false
   475  	for _, v := range signers {
   476  		if pk == v {
   477  			ok = true
   478  			break
   479  		}
   480  	}
   481  	return ok, nil
   482  }
   483  
   484  func GetOperatorSigners(ctx ActionCtx) ([]string, error) {
   485  	oc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  	var signers []string
   490  	if !oc.StrictSigningKeyUsage {
   491  		signers = append(signers, oc.Subject)
   492  	}
   493  	signers = append(signers, oc.SigningKeys...)
   494  	return signers, nil
   495  }
   496  
   497  func diffDates(format string, a, b int64) store.Status {
   498  	if a != b {
   499  		as := "always"
   500  		if a != 0 {
   501  			as = UnixToDate(a)
   502  		}
   503  		bs := "always"
   504  		if b != 0 {
   505  			bs = UnixToDate(b)
   506  		}
   507  		v := fmt.Sprintf("from %s to %s", as, bs)
   508  		return store.NewServerMessage(format, v)
   509  	}
   510  	return nil
   511  }
   512  
   513  func limitToString(v int64) string {
   514  	switch v {
   515  	case -1:
   516  		return "unlimited"
   517  	default:
   518  		return fmt.Sprintf("%d", v)
   519  	}
   520  }
   521  
   522  func diffNumber(format string, a, b int64) store.Status {
   523  	if a != b {
   524  		as := limitToString(a)
   525  		bs := limitToString(b)
   526  		v := fmt.Sprintf("from %s to %s", as, bs)
   527  		return store.NewServerMessage(format, v)
   528  	}
   529  	return nil
   530  }
   531  
   532  func diffBool(format string, a, b bool) store.Status {
   533  	if a != b {
   534  		v := fmt.Sprintf("from %t to %t", a, b)
   535  		return store.NewServerMessage(format, v)
   536  	}
   537  	return nil
   538  }
   539  
   540  func DiffAccountLimits(a *jwt.AccountClaims, b *jwt.AccountClaims) store.Status {
   541  	r := store.NewReport(store.WARN, "account server modifications")
   542  	r.Add(diffDates("jwt start changed %s", a.NotBefore, b.NotBefore))
   543  	r.Add(diffDates("jwt expiry changed %s", a.NotBefore, b.NotBefore))
   544  	r.Add(diffNumber("max subscriptions changed %s", a.Limits.Subs, b.Limits.Subs))
   545  	r.Add(diffNumber("max connections changed %s", a.Limits.Conn, b.Limits.Conn))
   546  	r.Add(diffNumber("max leaf node connections changed %s", a.Limits.LeafNodeConn, b.Limits.LeafNodeConn))
   547  	r.Add(diffNumber("max imports changed %s", a.Limits.Imports, b.Limits.Imports))
   548  	r.Add(diffNumber("max exports changed %s", a.Limits.Exports, b.Limits.Exports))
   549  	r.Add(diffNumber("max data changed %s", a.Limits.Data, b.Limits.Data))
   550  	r.Add(diffNumber("max message payload changed %s", a.Limits.Payload, b.Limits.Payload))
   551  	r.Add(diffBool("allow wildcard exports changed %s", a.Limits.WildcardExports, b.Limits.WildcardExports))
   552  	if len(r.Details) == 0 {
   553  		return nil
   554  	}
   555  	return r
   556  }
   557  
   558  func StoreAccountAndUpdateStatus(ctx ActionCtx, token string, status *store.Report) {
   559  	rs, err := ctx.StoreCtx().Store.StoreClaim([]byte(token))
   560  	// the order of the messages benefits from adding the status first
   561  	if rs != nil {
   562  		status.Add(rs)
   563  	}
   564  	if err != nil {
   565  		status.AddFromError(err)
   566  	}
   567  }
   568  
   569  func promptDuration(label string, defaultValue time.Duration) (time.Duration, error) {
   570  	value, err := cli.Prompt(label, defaultValue.String())
   571  	if err != nil {
   572  		return time.Duration(0), err
   573  	}
   574  	if value == "" {
   575  		return time.Duration(0), nil
   576  	}
   577  	return time.ParseDuration(value)
   578  }
   579  
   580  type dateTime int64
   581  
   582  func (t *dateTime) Set(val string) error {
   583  	if strings.TrimSpace(val) == "0" {
   584  		*t = 0
   585  		return nil
   586  	}
   587  	if v, err := time.Parse(time.RFC3339, val); err == nil {
   588  		*t = dateTime(v.Unix())
   589  		return nil
   590  	}
   591  	num, err := strconv.ParseInt(val, 10, 64)
   592  	if err != nil {
   593  		return fmt.Errorf("provided value %q is not a number nor parsable as RFC3339", val)
   594  	}
   595  	*t = dateTime(num)
   596  	return nil
   597  }
   598  
   599  func (t *dateTime) String() string {
   600  	v := time.Unix(int64(*t), 0)
   601  	return v.Format(time.RFC3339)
   602  }
   603  
   604  func (t *dateTime) Type() string {
   605  	return "date-time"
   606  }
   607  
   608  func Debug(label string, v interface{}) {
   609  	d, err := json.MarshalIndent(v, "", " ")
   610  	if err != nil {
   611  		panic(err)
   612  	}
   613  	fmt.Println(label, string(d))
   614  }
   615  
   616  var (
   617  	englishCaser cases.Caser
   618  	caserOnce    sync.Once
   619  )
   620  
   621  func TitleCase(s string) string {
   622  	caserOnce.Do(func() {
   623  		englishCaser = cases.Title(language.English)
   624  	})
   625  	return englishCaser.String(s)
   626  }