diff --git a/cmd/gcore-cli/main.go b/cmd/gcore-cli/main.go index ddacc4a..0ab2b2c 100644 --- a/cmd/gcore-cli/main.go +++ b/cmd/gcore-cli/main.go @@ -1,9 +1,10 @@ package main import ( + "github.com/G-core/gcore-cli/internal/commands" "github.com/G-core/gcore-cli/internal/core" ) func main() { - core.Execute() + core.Execute(commands.Commands()) } diff --git a/go.mod b/go.mod index 6462cda..c5c5e57 100644 --- a/go.mod +++ b/go.mod @@ -3,50 +3,41 @@ module github.com/G-core/gcore-cli go 1.21.5 require ( + github.com/AlekSi/pointer v1.2.0 github.com/G-Core/FastEdge-client-sdk-go v0.2.0 github.com/alecthomas/assert v1.0.0 + github.com/docker/go-units v0.5.0 github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.16.0 github.com/golang-module/carbon/v2 v2.3.10 + github.com/iancoleman/strcase v0.3.0 github.com/spf13/cobra v1.8.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.18.2 golang.org/x/term v0.19.0 golang.org/x/text v0.14.0 + gopkg.in/yaml.v3 v3.0.1 ) +require github.com/spf13/pflag v1.0.5 // indirect + require ( github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/getkin/kin-openapi v0.123.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sergi/go-diff v1.2.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/sys v0.19.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 23c0b95..1acddc6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= +github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/G-Core/FastEdge-client-sdk-go v0.2.0 h1:OHibIpVO/7kEG1eKy+5i0jcb1urPNm9EKUdH84entpk= github.com/G-Core/FastEdge-client-sdk-go v0.2.0/go.mod h1:ggyUVhy8/OCMBY4nbm7n9qDoPioROCk4vHhDJq9w7qE= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= @@ -15,14 +17,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -33,12 +33,10 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/golang-module/carbon/v2 v2.3.10 h1:C25x4A4UrIch6bisV3j37eU+op5+cp4gw/Fffv5c/FA= github.com/golang-module/carbon/v2 v2.3.10/go.mod h1:XDALX7KgqmHk95xyLeaqX9/LJGbfLATyruTziq68SZ8= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= @@ -53,8 +51,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -62,14 +58,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -78,29 +70,16 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -108,14 +87,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= @@ -128,8 +101,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/commands/commands.go b/internal/commands/commands.go new file mode 100644 index 0000000..d7dc2eb --- /dev/null +++ b/internal/commands/commands.go @@ -0,0 +1,17 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/commands/config" + "github.com/G-core/gcore-cli/internal/commands/fastedge" + initCmd "github.com/G-core/gcore-cli/internal/commands/init" +) + +func Commands() []*cobra.Command { + return []*cobra.Command{ + fastedge.Commands(), + initCmd.Commands(), + config.Commands(), + } +} diff --git a/internal/commands/config/config.go b/internal/commands/config/config.go new file mode 100644 index 0000000..981e22c --- /dev/null +++ b/internal/commands/config/config.go @@ -0,0 +1,221 @@ +package config + +import ( + "fmt" + "slices" + + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/config" + "github.com/G-core/gcore-cli/internal/core" + "github.com/G-core/gcore-cli/internal/output" +) + +func Commands() *cobra.Command { + var cmd = &cobra.Command{ + Use: "config", + Short: "Configuration file management", + GroupID: "configuration", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + cmd.AddGroup(&cobra.Group{ + ID: "config_commands", + Title: "Configuration commands", + }) + + cmd.AddCommand(info(), get(), set(), unset(), dump(), profileCmd()) + return cmd +} + +func profileCmd() *cobra.Command { + var cmd = &cobra.Command{ + Use: "profile", + Short: "Commands to manage config profiles", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + cmd.AddCommand(listProfiles(), createProfileCmd(), switchProfileCmd(), deleteProfileCmd()) + return cmd +} + +func createProfileCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Aliases: []string{"c"}, + Short: "Create a profile", + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return nil, cobra.ShellCompDirectiveDefault + } + + return []string{"api-key=", "api-url="}, cobra.ShellCompDirectiveDefault + }, + + RunE: func(cmd *cobra.Command, args []string) (err error) { + if len(args) == 0 { + cmd.Help() + + return nil + } + + profileName := args[0] + ctx := cmd.Context() + cfg := core.ExtractConfig(ctx) + + _, exist := cfg.Profiles[profileName] + if exist { + return fmt.Errorf("profile '%s' already exists", profileName) + } + + var profile = &config.Profile{} + if len(args[1:]) != 0 { + argProfile, err := profileFromArgs(args[1:]) + if err != nil { + return err + } + + profile = config.MergeProfiles(profile, argProfile) + } + + if cfg.Profiles == nil { + cfg.Profiles = make(map[string]*config.Profile) + } + cfg.Profiles[profileName] = profile + path, err := core.ExtractConfigPath(ctx) + if err != nil { + return err + } + + return cfg.Save(path) + }, + } + + return cmd +} + +func deleteProfileCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Aliases: []string{"d"}, + ValidArgsFunction: core.ProfileCompletion, + Short: "Delete profile from the config", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + cmd.Help() + + return nil + } + + profileName := args[0] + ctx := cmd.Context() + cfg := core.ExtractConfig(ctx) + active := core.ExtractProfile(ctx) + + _, exist := cfg.Profiles[profileName] + if exist { + delete(cfg.Profiles, profileName) + } else { + return fmt.Errorf("profile '%s' doesn't exist", profileName) + } + + if active == profileName { + cfg.ActiveProfile = config.DefaultProfile + } + + path, err := core.ExtractConfigPath(ctx) + if err != nil { + return err + } + + return cfg.Save(path) + }, + } + + return cmd +} + +func listProfiles() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "Display list of available profiles in the config", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + cfg := core.ExtractConfig(ctx) + currentProfile := core.ExtractProfile(ctx) + + var profiles []profileView + if currentProfile == config.DefaultProfile { + profiles = append([]profileView{}, toProfileView("=> "+config.DefaultProfile, &cfg.Profile)) + } else { + profiles = append([]profileView{}, toProfileView(config.DefaultProfile, &cfg.Profile)) + } + + var names []string + for name, _ := range cfg.Profiles { + names = append(names, name) + } + slices.Sort(names) + + for _, name := range names { + var pv profileView + if name == currentProfile { + pv = toProfileView("=> "+name, cfg.Profiles[name]) + } else { + pv = toProfileView(name, cfg.Profiles[name]) + } + + profiles = append(profiles, pv) + } + + output.Print(profiles) + + return nil + }, + } + + return cmd +} + +func switchProfileCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "switch ", + ValidArgsFunction: core.ProfileCompletion, + Aliases: []string{"sw"}, + Short: "Make selected profile active", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + cmd.Help() + + return nil + } + + profileName := args[0] + ctx := cmd.Context() + cfg := core.ExtractConfig(ctx) + + _, exist := cfg.Profiles[profileName] + if exist { + cfg.ActiveProfile = profileName + } else if profileName != config.DefaultProfile { + return fmt.Errorf("profile '%s' doesn't exist", profileName) + } else { + cfg.ActiveProfile = config.DefaultProfile + } + + path, err := core.ExtractConfigPath(ctx) + if err != nil { + return err + } + + return cfg.Save(path) + }, + } + + return cmd +} diff --git a/internal/commands/config/dump.go b/internal/commands/config/dump.go new file mode 100644 index 0000000..fc8f1c6 --- /dev/null +++ b/internal/commands/config/dump.go @@ -0,0 +1,34 @@ +package config + +import ( + "github.com/AlekSi/pointer" + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/core" + "github.com/G-core/gcore-cli/internal/output" +) + +func dump() *cobra.Command { + var cmd = &cobra.Command{ + Use: "dump", + Short: "Dumps the config file", + Args: cobra.NoArgs, + GroupID: "config_commands", + RunE: func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + cfg := core.ExtractConfig(ctx) + + // Secure keys + cfg.Profile.ApiKey = pointer.To(secureKey(cfg.Profile.ApiKey)) + for _, profile := range cfg.Profiles { + profile.ApiKey = pointer.To(secureKey(profile.ApiKey)) + } + + output.Print(cfg) + + return nil + }, + } + + return cmd +} diff --git a/internal/commands/config/get.go b/internal/commands/config/get.go new file mode 100644 index 0000000..f9fdd6b --- /dev/null +++ b/internal/commands/config/get.go @@ -0,0 +1,75 @@ +package config + +import ( + "fmt" + "reflect" + + "github.com/iancoleman/strcase" + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/config" + "github.com/G-core/gcore-cli/internal/core" + "github.com/G-core/gcore-cli/internal/output" +) + +func getProfileField(profile *config.Profile, key string) (reflect.Value, error) { + field := reflect.ValueOf(profile).Elem().FieldByName(strcase.ToCamel(key)) + reflect.ValueOf(profile).Elem().FieldByNameFunc(func(s string) bool { + return key == strcase.ToKebab(s) + }) + + if !field.IsValid() { + return reflect.ValueOf(nil), fmt.Errorf("invalid key: %s", key) + } + + return field, nil +} + +func getProfileValue(profile *config.Profile, fieldName string) (interface{}, error) { + field, err := getProfileField(profile, fieldName) + if err != nil { + return nil, err + } + return field.Interface(), nil +} + +func get() *cobra.Command { + var cmd = &cobra.Command{ + Use: "get ", + Short: "Get property value from the active profile", + GroupID: "config_commands", + ValidArgs: []string{"api-url", "api-key"}, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + cmd.Help() + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + + ctx := cmd.Context() + profileName := core.ExtractProfile(ctx) + cfg := core.ExtractConfig(ctx) + + profile, err := cfg.GetProfile(profileName) + if err != nil { + return err + } + + value, err := getProfileValue(profile, args[0]) + if err != nil { + return err + } + + output.Print(value) + + return nil + }, + } + + return cmd +} diff --git a/internal/commands/config/info.go b/internal/commands/config/info.go new file mode 100644 index 0000000..190ee8c --- /dev/null +++ b/internal/commands/config/info.go @@ -0,0 +1,85 @@ +package config + +import ( + "strings" + + "github.com/AlekSi/pointer" + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/config" + "github.com/G-core/gcore-cli/internal/core" + "github.com/G-core/gcore-cli/internal/output" +) + +type profileView struct { + ProfileName string + ApiUrl *string + ApiKey *string +} + +type configInfo struct { + ConfigPath string + ProfileName string + Profile *config.Profile +} + +func toProfileView(name string, profile *config.Profile) profileView { + var pv = profileView{ + ProfileName: name, + } + + if profile.ApiUrl != nil { + pv.ApiUrl = profile.ApiUrl + } + + if profile.ApiKey != nil { + pv.ApiKey = pointer.To(secureKey(profile.ApiKey)) + } + + return pv +} + +func info() *cobra.Command { + var cmd = &cobra.Command{ + Use: "info", + Short: "Get information about active profile", + Args: cobra.NoArgs, + GroupID: "config_commands", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + profile, err := core.GetClientProfile(ctx) + if err != nil { + return err + } + + path, err := core.ExtractConfigPath(ctx) + if err != nil { + return err + } + + output.Print(&configInfo{ + ConfigPath: path, + ProfileName: core.ExtractProfile(ctx), + Profile: profile, + }) + + return nil + }, + } + + return cmd +} + +func secureKey(key *string) string { + if key == nil || *key == "" { + return "" + } + + var p1 = 0 + 5 + var p2 = len(*key) - 1 - 5 + if p1 > p2 { + return "XXXXXX" + } + + return strings.Join([]string{(*key)[0:p1], "XXXXXX", (*key)[p2 : len((*key))-1]}, "") +} diff --git a/internal/commands/config/set.go b/internal/commands/config/set.go new file mode 100644 index 0000000..3ae95cf --- /dev/null +++ b/internal/commands/config/set.go @@ -0,0 +1,115 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/AlekSi/pointer" + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/config" + "github.com/G-core/gcore-cli/internal/core" + "github.com/G-core/gcore-cli/internal/output" +) + +func profileFromArgs(args []string) (*config.Profile, error) { + var m = make(map[string]any) + for _, arg := range args { + ss := strings.Split(arg, "=") + if len(ss) != 2 { + continue + } + + name, value := ss[0], ss[1] + // TODO: reflection here + switch name { + case "api-url", "api-key": + m[name] = &value + } + } + + if len(m) == 0 { + return nil, fmt.Errorf("invalid arguments") + } + + var p config.Profile + for name, value := range m { + switch name { + case "api-url": + p.ApiUrl = value.(*string) + case "api-key": + p.ApiKey = value.(*string) + } + } + + return &p, nil +} + +func set() *cobra.Command { + var p config.Profile + var cmd = &cobra.Command{ + Use: "set =", + Short: "Set property for the active profile", + Long: "This commands overwrites the configuration file parameters with user input.\n" + + "The only allowed arguments are: api-url, api-key", + GroupID: "config_commands", + ValidArgs: []string{"api-url", "api-key"}, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + cmd.Help() + + return nil + } + + argProfile, err := profileFromArgs(args) + if err != nil { + return err + } + + p = *argProfile + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + + ctx := cmd.Context() + cfg := core.ExtractConfig(ctx) + profileName := core.ExtractProfile(ctx) + profile := &cfg.Profile + if profileName != config.DefaultProfile { + var exist bool + profile, exist = cfg.Profiles[profileName] + if !exist { + if cfg.Profiles == nil { + cfg.Profiles = map[string]*config.Profile{} + } + cfg.Profiles[profileName] = &config.Profile{} + profile = cfg.Profiles[profileName] + } + } + + profile = config.MergeProfiles(profile, &p) + cfg.SetProfile(profileName, profile) + + path, err := core.ExtractConfigPath(ctx) + if err != nil { + return err + } + + if err := cfg.Save(path); err != nil { + return err + } + + profile, _ = cfg.GetProfile(profileName) + profile.ApiKey = pointer.To(secureKey(profile.ApiKey)) + output.Print(profile) + + return nil + }, + } + + return cmd +} diff --git a/internal/commands/config/unset.go b/internal/commands/config/unset.go new file mode 100644 index 0000000..fb00344 --- /dev/null +++ b/internal/commands/config/unset.go @@ -0,0 +1,58 @@ +package config + +import ( + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/core" + "github.com/G-core/gcore-cli/internal/output" +) + +func unset() *cobra.Command { + var cmd = &cobra.Command{ + Use: "unset ", + Short: "Reset property in the active profile", + Long: "Resets property in the active profile. If property was reset the value for it will be taken from default profile.\n" + + "The only allowed arguments are: api-url, api-key", + ValidArgs: []string{"api-url", "api-key"}, + GroupID: "config_commands", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + cmd.Help() + + return nil + } + + ctx := cmd.Context() + profileName := core.ExtractProfile(ctx) + cfg := core.ExtractConfig(ctx) + profile, err := cfg.GetProfile(profileName) + if err != nil { + return err + } + + for _, name := range args { + switch name { + case "api-url": + profile.ApiUrl = nil + case "api-key": + profile.ApiKey = nil + } + } + + cfg.SetProfile(profileName, profile) + path, err := core.ExtractConfigPath(ctx) + if err != nil { + return err + } + + if err := cfg.Save(path); err != nil { + return err + } + + output.Print(profile) + return nil + }, + } + + return cmd +} diff --git a/internal/commands/fastedge/app.go b/internal/commands/fastedge/app.go index f4ea89e..5d55a60 100644 --- a/internal/commands/fastedge/app.go +++ b/internal/commands/fastedge/app.go @@ -34,6 +34,7 @@ You can use either previously-uploaded binary, by specifying "--binary ", or uploading binary using "--file ". To load file from stdin, use "-" as filename`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() app, err := parseAppProperties(cmd) if err != nil { return err @@ -46,14 +47,14 @@ uploading binary using "--file ". To load file from stdin, use "-" as if file == "" { return errors.New("binary must be specified either using --binary or --file ") } - id, err := uploadBinary(file) + id, err := uploadBinary(ctx, file) if err != nil { return err } app.Binary = &id } - rsp, err := client.AddAppWithResponse(context.Background(), app) + rsp, err := client.AddAppWithResponse(ctx, app) if err != nil { return fmt.Errorf("adding the app: %w", err) } @@ -89,7 +90,8 @@ You can use either previously-uploaded binary, by specifying "--binary ", or uploading binary using "--file ". To load file from stdin, use "-" as filename`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } @@ -104,7 +106,7 @@ uploading binary using "--file ". To load file from stdin, use "-" as return fmt.Errorf("cannot parse file name: %w", err) } if file != "" { - id, err := uploadBinary(file) + id, err := uploadBinary(ctx, file) if err != nil { return err } @@ -116,7 +118,7 @@ uploading binary using "--file ". To load file from stdin, use "-" as return e.ErrAborted } - rsp, err := client.PatchAppWithResponse(context.Background(), id, app) + rsp, err := client.PatchAppWithResponse(ctx, id, app) if err != nil { return fmt.Errorf("updating the app: %w", err) } @@ -147,7 +149,7 @@ uploading binary using "--file ". To load file from stdin, use "-" as Short: "Show list of client's apps", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - rsp, err := client.ListAppsWithResponse(context.Background(), &sdk.ListAppsParams{}) + rsp, err := client.ListAppsWithResponse(cmd.Context(), &sdk.ListAppsParams{}) if err != nil { return fmt.Errorf("getting the list of apps: %w", err) } @@ -189,12 +191,13 @@ To see statistics, use "fastedge stats app_calls" and "fastedge stats app_durati commands.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } rsp, err := client.GetAppWithResponse( - context.Background(), + ctx, id, ) if err != nil { @@ -231,12 +234,13 @@ commands.`, Short: "Enable the app", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } rsp, err := client.PatchAppWithResponse( - context.Background(), + ctx, id, sdk.App{Status: newPointer(1)}, ) @@ -262,12 +266,13 @@ commands.`, Short: "Disable the app", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } rsp, err := client.PatchAppWithResponse( - context.Background(), + ctx, id, sdk.App{Status: newPointer(2)}, ) @@ -297,7 +302,8 @@ however binaries, not referenced by any app, get deleted by cleanup process regu so if you don't want this to happen, consider disabling the app to keep binary referenced`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } @@ -306,7 +312,7 @@ so if you don't want this to happen, consider disabling the app to keep binary r return e.ErrAborted } - rsp, err := client.DelAppWithResponse(context.Background(), id) + rsp, err := client.DelAppWithResponse(ctx, id) if err != nil { return fmt.Errorf("deleting app: %w", err) } @@ -434,8 +440,8 @@ func outputMap(m *map[string]string, title string) { } } -func getAppIdByName(appName string) (int64, error) { - idRsp, err := client.ListAppsWithResponse(context.Background(), &sdk.ListAppsParams{Name: &appName}) +func getAppIdByName(ctx context.Context, appName string) (int64, error) { + idRsp, err := client.ListAppsWithResponse(ctx, &sdk.ListAppsParams{Name: &appName}) if err != nil { return 0, fmt.Errorf("api response: %w", err) } diff --git a/internal/commands/fastedge/binary.go b/internal/commands/fastedge/binary.go index 4a822ef..4a608c9 100644 --- a/internal/commands/fastedge/binary.go +++ b/internal/commands/fastedge/binary.go @@ -32,7 +32,8 @@ func binary() *cobra.Command { Short: "Show list of client's binaries", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - rsp, err := client.ListBinariesWithResponse(context.Background(), &sdk.ListBinariesParams{}) + ctx := cmd.Context() + rsp, err := client.ListBinariesWithResponse(ctx, &sdk.ListBinariesParams{}) if err != nil { return fmt.Errorf("getting the list of binaries: %w", err) } @@ -74,12 +75,13 @@ func binary() *cobra.Command { If this flag is omitted, file contant is read from stdin.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() src, err := cmd.Flags().GetString("file") if err != nil { return errors.New("please specify binary filename") } - id, err := uploadBinary(src) + id, err := uploadBinary(ctx, src) if err != nil { return err } @@ -97,12 +99,13 @@ If this flag is omitted, file contant is read from stdin.`, Short: "Show binary details", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() id, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return fmt.Errorf("parsing binary id: %w", err) } - rsp, err := client.GetBinaryWithResponse(context.Background(), id) + rsp, err := client.GetBinaryWithResponse(ctx, id) if err != nil { return fmt.Errorf("getting the list of plans: %w", err) } @@ -144,12 +147,13 @@ If this flag is omitted, file contant is read from stdin.`, Long: `Delete the binary. Binary cannot be deleted if it is still referenced by any app.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() id, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return fmt.Errorf("parsing binary id: %w", err) } - rsp, err := client.DelBinaryWithResponse(context.Background(), id) + rsp, err := client.DelBinaryWithResponse(ctx, id) if err != nil { return fmt.Errorf("getting the list of plans: %w", err) } @@ -172,7 +176,7 @@ If this flag is omitted, file contant is read from stdin.`, return cmdBin } -func uploadBinary(src string) (int64, error) { +func uploadBinary(ctx context.Context, src string) (int64, error) { r := os.Stdin var err error if src != sourceStdin { @@ -184,7 +188,7 @@ func uploadBinary(src string) (int64, error) { } rsp, err := client.StoreBinaryWithBodyWithResponse( - context.Background(), + ctx, wasmContentType, r, ) diff --git a/internal/commands/fastedge/fastedge.go b/internal/commands/fastedge/fastedge.go index 82982bd..f167c0b 100644 --- a/internal/commands/fastedge/fastedge.go +++ b/internal/commands/fastedge/fastedge.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" sdk "github.com/G-Core/FastEdge-client-sdk-go" + "github.com/G-core/gcore-cli/internal/core" ) const ( @@ -22,19 +23,29 @@ const ( var client *sdk.ClientWithResponses // top-level FastEdge command -func Commands(baseUrl string, authFunc func(ctx context.Context, req *http.Request) error) (*cobra.Command, error) { - var local bool +func Commands() *cobra.Command { var cmdFastedge = &cobra.Command{ - Use: "fastedge ", - Short: "Gcore Edge compute solution", - Long: ``, - Args: cobra.MinimumNArgs(1), + Use: "fastedge ", + Short: "Gcore Edge compute solution", + Long: ``, + GroupID: "fastedge", + Args: cobra.MinimumNArgs(1), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - var err error - url := baseUrl - if !local { + var ( + err error + ctx = cmd.Context() + ) + profile, err := core.GetClientProfile(ctx) + if err != nil { + return err + } + url := *profile.ApiUrl + authFunc := core.ExtractAuthFunc(ctx) + + if !profile.IsLocal() { url += "/fastedge" } + client, err = sdk.NewClientWithResponses( url, sdk.WithRequestEditorFn(authFunc), @@ -52,11 +63,9 @@ func Commands(baseUrl string, authFunc func(ctx context.Context, req *http.Reque return nil }, } - cmdFastedge.PersistentFlags().BoolVar(&local, "local", false, "local testing") - cmdFastedge.PersistentFlags().MarkHidden("local") cmdFastedge.AddCommand(app(), binary(), stat(), logs()) - return cmdFastedge, nil + return cmdFastedge } func newPointer[T any](val T) *T { diff --git a/internal/commands/fastedge/logs.go b/internal/commands/fastedge/logs.go index f7aec02..209abf1 100644 --- a/internal/commands/fastedge/logs.go +++ b/internal/commands/fastedge/logs.go @@ -2,7 +2,6 @@ package fastedge import ( "bufio" - "context" "errors" "fmt" "net/http" @@ -89,13 +88,14 @@ This command allows you filtering by edge name, client ip and time range.`, return nil }, RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } rsp, err := client.ListLogsWithResponse( - context.Background(), + ctx, id, &sdk.ListLogsParams{ From: &from, @@ -146,7 +146,7 @@ This command allows you filtering by edge name, client ip and time range.`, // Call the API again with the new page number rsp, err = client.ListLogsWithResponse( - context.Background(), + ctx, id, &sdk.ListLogsParams{ From: &from, @@ -178,12 +178,13 @@ This command allows you filtering by edge name, client ip and time range.`, Short: "Enable app logging", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } rsp, err := client.PatchAppWithResponse( - context.Background(), + ctx, id, sdk.App{Debug: newPointer(true)}, ) @@ -195,7 +196,7 @@ This command allows you filtering by edge name, client ip and time range.`, } rsp1, err := client.GetAppWithResponse( - context.Background(), + ctx, id, ) if err != nil { @@ -219,12 +220,13 @@ This command allows you filtering by edge name, client ip and time range.`, Short: "Disable app logging", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - id, err := getAppIdByName(args[0]) + ctx := cmd.Context() + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } rsp, err := client.PatchAppWithResponse( - context.Background(), + ctx, id, sdk.App{Debug: newPointer(false)}, ) diff --git a/internal/commands/fastedge/stats.go b/internal/commands/fastedge/stats.go index 23d1c1d..e781fb8 100644 --- a/internal/commands/fastedge/stats.go +++ b/internal/commands/fastedge/stats.go @@ -1,7 +1,6 @@ package fastedge import ( - "context" "fmt" "net/http" "slices" @@ -21,7 +20,8 @@ func stat() *cobra.Command { Short: "Statistics", Args: cobra.MinimumNArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - rsp, err := client.GetClientMeWithResponse(context.Background()) + ctx := cmd.Context() + rsp, err := client.GetClientMeWithResponse(ctx) if err != nil { return fmt.Errorf("getting the statistics: %w", err) } @@ -68,8 +68,9 @@ can be omitted, or as UNIX timestamp) and reporting step duration with flag Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var appId *int64 + ctx := cmd.Context() if len(args) > 0 { - id, err := getAppIdByName(args[0]) + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } @@ -92,7 +93,7 @@ can be omitted, or as UNIX timestamp) and reporting step duration with flag } rsp, err := client.StatsCallsWithResponse( - context.Background(), + ctx, &sdk.StatsCallsParams{ Id: appId, From: from, @@ -184,8 +185,9 @@ can be omitted, or as UNIX timestamp) and reporting step duration with flag Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var appId *int64 + ctx := cmd.Context() if len(args) > 0 { - id, err := getAppIdByName(args[0]) + id, err := getAppIdByName(ctx, args[0]) if err != nil { return fmt.Errorf("cannot find app by name: %w", err) } @@ -208,7 +210,7 @@ can be omitted, or as UNIX timestamp) and reporting step duration with flag } rsp, err := client.StatsDurationWithResponse( - context.Background(), + ctx, &sdk.StatsDurationParams{ Id: appId, From: from, diff --git a/internal/commands/init/init.go b/internal/commands/init/init.go new file mode 100644 index 0000000..12a864b --- /dev/null +++ b/internal/commands/init/init.go @@ -0,0 +1,75 @@ +package init + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/config" + "github.com/G-core/gcore-cli/internal/core" + "github.com/G-core/gcore-cli/internal/errors" + "github.com/G-core/gcore-cli/internal/sure" +) + +func Commands() *cobra.Command { + var cmd = &cobra.Command{ + Use: "init ", + Short: "Initialize the config for gcore-cli", + Long: `Initialize the active profile of the config. +Default path for configuration file is based on the following priority order: +- $GCORE_CONFIG +- $HOME/.gcorecli/config.yaml +`, + GroupID: "configuration", + Example: "gcore init -p prod", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + profileName := core.ExtractProfile(ctx) + cfg := core.ExtractConfig(ctx) + + var profile = &cfg.Profile + if profileName != config.DefaultProfile { + _, found := cfg.Profiles[profileName] + if !found { + if cfg.Profiles == nil { + cfg.Profiles = make(map[string]*config.Profile) + } + cfg.Profiles[profileName] = &config.Profile{} + } + profile = cfg.Profiles[profileName] + } + + // Do not ask if it's default profile, and it isn't initialized + if profileName != config.DefaultProfile || profile.IsInitialized() { + // TODO: Interactive output should be in stderror + if !sure.AreYou(cmd, fmt.Sprintf("overwrite profile '%s'", profileName)) { + return errors.ErrAborted + } + } + + profile.ApiKey = askForApiKey(cmd) + path, err := core.ExtractConfigPath(ctx) + if err != nil { + return err + } + + return cfg.Save(path) + }, + } + + cmd.PersistentFlags().String("apikey", "", "GCore API key") + + return cmd +} + +func askForApiKey(cmd *cobra.Command) *string { + apikey, _ := cmd.PersistentFlags().GetString("apikey") + if apikey == "" { + fmt.Printf("Please, enter API key: ") + fmt.Scanf("%s", &apikey) + } + + return &apikey +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..43709bd --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "fmt" + "os" + "path" +) + +const CliConfigFile = "config.yaml" + +func getConfigHomeDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("getting user home directory: %w", err) + } + + return path.Join(home, ".gcorecli"), nil +} + +func GetConfigPath() (string, error) { + configDir, err := getConfigHomeDir() + if err != nil { + return "", err + } + + return path.Join(configDir, CliConfigFile), nil +} diff --git a/internal/config/types.go b/internal/config/types.go new file mode 100644 index 0000000..065886f --- /dev/null +++ b/internal/config/types.go @@ -0,0 +1,159 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/AlekSi/pointer" + "gopkg.in/yaml.v3" +) + +const ( + DefaultProfile = "default" + DefaultAPI = "https://api.gcore.com" +) + +const ( + EnvConfigPath = "GCORE_CONFIG" + EnvConfigProfile = "GCORE_PROFILE" + EnvProfileURL = "GCORE_API_URL" + EnvProfileAPIKey = "GCORE_API_KEY" +) + +type Profile struct { + ApiUrl *string `yaml:"api-url,omitempty" json:"api-url,omitempty"` + ApiKey *string `yaml:"api-key,omitempty" json:"api-key,omitempty"` +} + +func (p *Profile) IsInitialized() bool { + return p.ApiKey != nil && *p.ApiKey != "" +} + +func (p *Profile) IsLocal() bool { + if p.ApiUrl == nil { + return false + } + + if *p.ApiUrl == DefaultAPI { + return false + } + + return true +} + +type Config struct { + Profile `yaml:",inline"` + ActiveProfile string `yaml:"profile" json:"profile,omitempty"` + Profiles map[string]*Profile `yaml:"profiles,omitempty" json:"profiles,omitempty"` +} + +func NewDefault() *Config { + return &Config{ + ActiveProfile: DefaultProfile, + Profile: Profile{ + ApiUrl: pointer.To(DefaultAPI), + }, + } +} + +func (c *Config) String() string { + body, _ := yaml.Marshal(c) + + return string(body) +} + +func (c *Config) Load(path string) error { + body, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + if err := yaml.Unmarshal(body, c); err != nil { + return fmt.Errorf("failed to unmarshal config: %w", err) + } + + return nil +} + +func (c *Config) Save(path string) error { + body, err := yaml.Marshal(*c) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + + if err := os.WriteFile(path, body, 0644); err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + + return nil +} + +func (c *Config) GetProfile(name string) (*Profile, error) { + if name == DefaultProfile { + return &c.Profile, nil + } + + if c.Profiles == nil { + return nil, fmt.Errorf("profile '%s' isn't exist", name) + } + + p, exist := c.Profiles[name] + if !exist { + return nil, fmt.Errorf("profile '%s' isn't exist", name) + } + + return MergeProfiles(&c.Profile, p), nil +} + +func (c *Config) SetProfile(name string, profile *Profile) { + if name == DefaultProfile { + c.Profile = *profile + + return + } + + if c.Profiles == nil { + c.Profiles = map[string]*Profile{} + } + + c.Profiles[name] = profile +} + +func GetEnvProfile() *Profile { + var profile Profile + + if url := os.Getenv(EnvProfileURL); url != "" { + profile.ApiUrl = &url + } + + if apiKey := os.Getenv(EnvProfileAPIKey); apiKey != "" { + profile.ApiKey = &apiKey + } + + return &profile +} + +func MergeProfiles(original *Profile, profiles ...*Profile) *Profile { + var result = &Profile{ + ApiKey: original.ApiKey, + ApiUrl: original.ApiUrl, + } + + for _, profile := range profiles { + if profile.ApiKey != nil { + result.ApiKey = pointer.To(*profile.ApiKey) + } + + if profile.ApiUrl != nil { + result.ApiUrl = pointer.To(*profile.ApiUrl) + } + } + + return result +} diff --git a/internal/core/completion.go b/internal/core/completion.go new file mode 100644 index 0000000..28579a0 --- /dev/null +++ b/internal/core/completion.go @@ -0,0 +1,24 @@ +package core + +import ( + "strings" + + "github.com/spf13/cobra" + + "github.com/G-core/gcore-cli/internal/config" +) + +func ProfileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ctx := cmd.Context() + + cfg := ExtractConfig(ctx) + var completions []string + completions = append(completions, config.DefaultProfile) + for name, _ := range cfg.Profiles { + if strings.HasPrefix(name, toComplete) { + completions = append(completions, name) + } + } + + return completions, cobra.ShellCompDirectiveDefault +} diff --git a/internal/core/meta.go b/internal/core/meta.go new file mode 100644 index 0000000..b85615e --- /dev/null +++ b/internal/core/meta.go @@ -0,0 +1,108 @@ +package core + +import ( + "context" + "net/http" + "os" + + "github.com/G-core/gcore-cli/internal/config" +) + +const metaKey = iota + +// meta contains information about global flags and cli configuration +type meta struct { + cfg *config.Config + ctx context.Context + + // Global flags + flagConfig string + flagProfile string + flagForce bool + flagWait bool + flagAPIURL string + flagAPIKey string + + // Auth function + authFunc func(ctx context.Context, req *http.Request) error +} + +func injectMeta(ctx context.Context, m meta) context.Context { + return context.WithValue(ctx, metaKey, m) +} + +func extractMeta(ctx context.Context) meta { + return ctx.Value(metaKey).(meta) +} + +func ExtractConfig(ctx context.Context) *config.Config { + return extractMeta(ctx).cfg +} + +func ExtractConfigPath(ctx context.Context) (string, error) { + path := extractMeta(ctx).flagConfig + if len(path) != 0 { + return path, nil + } + + path = os.Getenv(config.EnvConfigPath) + if len(path) != 0 { + return path, nil + } + + return config.GetConfigPath() +} + +func ExtractProfile(ctx context.Context) string { + profileName := extractMeta(ctx).flagProfile + if len(profileName) > 0 { + return profileName + } + + profile := os.Getenv(config.EnvConfigProfile) + if len(profile) > 0 { + return profile + } + + cfg := ExtractConfig(ctx) + if len(cfg.ActiveProfile) > 0 { + return cfg.ActiveProfile + } + + return config.DefaultProfile +} + +// GetFlagProfile makes profile from cli flags +func GetFlagProfile(ctx context.Context) *config.Profile { + meta := extractMeta(ctx) + profile := &config.Profile{} + if meta.flagAPIURL != "" { + profile.ApiUrl = &meta.flagAPIURL + } + + if meta.flagAPIKey != "" { + profile.ApiKey = &meta.flagAPIKey + } + + return profile +} + +// GetClientProfile returns current profile for client merged from config, envs and flag variables +func GetClientProfile(ctx context.Context) (*config.Profile, error) { + name := ExtractProfile(ctx) + cfg := ExtractConfig(ctx) + + profile, err := cfg.GetProfile(name) + if err != nil { + return nil, err + } + + envProfile := config.GetEnvProfile() + flagProfile := GetFlagProfile(ctx) + + return config.MergeProfiles(profile, envProfile, flagProfile), nil +} + +func ExtractAuthFunc(ctx context.Context) func(ctx context.Context, req *http.Request) error { + return extractMeta(ctx).authFunc +} diff --git a/internal/core/root.go b/internal/core/root.go index 1eddf46..6fba06a 100644 --- a/internal/core/root.go +++ b/internal/core/root.go @@ -8,71 +8,91 @@ import ( "strings" "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/G-core/gcore-cli/internal/commands/fastedge" + "github.com/G-core/gcore-cli/internal/config" "github.com/G-core/gcore-cli/internal/errors" "github.com/G-core/gcore-cli/internal/human" "github.com/G-core/gcore-cli/internal/output" ) -func Execute() { +func init() { + cobra.EnableCommandSorting = false +} + +func Execute(commands []*cobra.Command) { var rootCmd = &cobra.Command{ // TODO: pick name from binary name - Use: "gcore-cli", + Use: os.Args[0], SilenceUsage: true, SilenceErrors: true, } + var meta meta + // global flags, applicable to all sub-commands - apiKey := rootCmd.PersistentFlags().StringP("apikey", "a", "", "API key") - apiUrl := rootCmd.PersistentFlags().StringP("url", "u", "https://api.gcore.com", "API URL") - rootCmd.PersistentFlags().BoolP("force", "f", false, `Assume answer "yes" to all "are you sure?" questions`) - rootCmd.PersistentFlags().IntP("project", "", 0, "Cloud project ID") - rootCmd.PersistentFlags().IntP("region", "", 0, "Cloud region ID") - rootCmd.PersistentFlags().BoolP("wait", "", false, "Wait for command result") + rootCmd.PersistentFlags().StringVarP(&meta.flagAPIKey, "apikey", "a", "", "API key") + rootCmd.PersistentFlags().StringVarP(&meta.flagAPIURL, "url", "u", "https://api.gcore.com", "API URL") + rootCmd.PersistentFlags().StringVarP(&meta.flagConfig, "config", "c", "", "The path to the config file") + rootCmd.PersistentFlags().BoolVarP(&meta.flagForce, "force", "f", false, `Assume answer "yes" to all "are you sure?" questions`) + rootCmd.PersistentFlags().StringVarP(&meta.flagProfile, "profile", "p", "", "The config profile to use") + rootCmd.RegisterFlagCompletionFunc("profile", ProfileCompletion) + rootCmd.PersistentFlags().BoolVarP(&meta.flagWait, "wait", "w", false, "Wait for command result") + output.FormatOption(rootCmd) rootCmd.ParseFlags(os.Args[1:]) - v := viper.New() - v.SetEnvPrefix("gcore") - v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - v.AutomaticEnv() - bindFlags(rootCmd, v) + meta.cfg = GetConfig() + meta.authFunc = func(ctx context.Context, req *http.Request) error { + profile, err := GetClientProfile(ctx) + if err != nil { + return err + } - authFunc := func(ctx context.Context, req *http.Request) error { - req.Header.Set("Authorization", "APIKey "+*apiKey) + if profile.ApiKey == nil || *profile.ApiKey == "" { + return &errors.CliError{ + Err: fmt.Errorf("subcommand requires authorization"), + Hint: "See gcore-cli init, gcore-cli config", + } + } + + req.Header.Set("Authorization", "APIKey "+*profile.ApiKey) return nil } + meta.ctx = injectMeta(context.Background(), meta) + rootCmd.SetContext(meta.ctx) + rootCmd.AddGroup(&cobra.Group{ + ID: "fastedge", + Title: "FastEdge commands", + }, &cobra.Group{ + ID: "configuration", + Title: "Configuration commands", + }) + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - for _, safeCmd := range []string{"completion", "help"} { + for _, safeCmd := range []string{"init", "config", "completion", "help"} { if strings.Contains(cmd.CommandPath(), safeCmd) { return nil } } - if *apiUrl == "" { - return &errors.CliError{ - Message: "URL for API isn't specified", - Hint: "You can specify it by -u flag or GCORE_URL env variable", - Code: 1, - } + + profile, err := GetClientProfile(cmd.Context()) + if err != nil { + return err } - if *apiKey == "" { + if profile.ApiUrl == nil && *profile.ApiUrl == "" { return &errors.CliError{ - Message: "API key must be specified", - Hint: "You can specify it with -a flag or GCORE_APIKEY env variable.\n" + - "To get an APIKEY visit https://accounts.gcore.com/profile/api-tokens", + Err: fmt.Errorf("URL for API isn't specified"), + Hint: "You can specify it by -u flag or GCORE_API_URL env variable", Code: 1, } } - if !strings.Contains(*apiKey, "$") { + if !strings.Contains(*profile.ApiKey, "$") { return &errors.CliError{ Message: "Malformed API key", - Hint: "If you specified API key using '-a' option and GCORE_APIKEY env variable,\n" + + Hint: "If you specified API key using '-a' option and GCORE_API_KEY env variable,\n" + "please make sure that you are using single quotes to prevent shell\n" + "parameter expansion", Code: 1, @@ -82,15 +102,12 @@ func Execute() { return nil } - fastedgeCmd, err := fastedge.Commands(*apiUrl, authFunc) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed: %v\n", err) - os.Exit(1) + for _, command := range commands { + rootCmd.AddCommand(command) } - rootCmd.AddCommand(fastedgeCmd) cobra.EnableTraverseRunHooks = true // make sure all parentPersistentPreRun executed - err = rootCmd.Execute() + err := rootCmd.Execute() if err != nil { cliErr, ok := err.(*errors.CliError) if !ok { @@ -104,12 +121,25 @@ func Execute() { } } -func bindFlags(cmd *cobra.Command, v *viper.Viper) { - cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { - // Apply the viper config value to the flag when the flag is not set and viper has a value - if !f.Changed && v.IsSet(f.Name) { - val := v.Get(f.Name) - cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) +// GetConfig tries to load config from $HOME dir. +// If config doesn't exist - returns default config. +func GetConfig() *config.Config { + var ( + err error + cfg config.Config + ) + + path := os.Getenv(config.EnvConfigPath) + if len(path) == 0 { + path, err = config.GetConfigPath() + if err != nil { + return config.NewDefault() } - }) + } + + if err := cfg.Load(path); err != nil { + return config.NewDefault() + } + + return &cfg } diff --git a/internal/human/specs.go b/internal/human/specs.go index 60f5bc9..37809ce 100644 --- a/internal/human/specs.go +++ b/internal/human/specs.go @@ -4,6 +4,8 @@ package human import ( "strings" + + "github.com/iancoleman/strcase" ) // MarshalOpt is hydrated by core.View @@ -39,7 +41,7 @@ func (s *MarshalFieldOpt) getLabel() string { label := s.FieldName label = strings.ReplaceAll(label, ".", " ") - //label = strcase.ToBashArg(label) + label = strcase.ToKebab(label) label = strings.ReplaceAll(label, "-", " ") label = strings.ToUpper(label) return label