From 03cf0ead13ae8c94703cda816bd7281a1c36db36 Mon Sep 17 00:00:00 2001 From: Daniel De Vera Date: Fri, 24 Apr 2026 16:04:03 -0300 Subject: [PATCH 1/2] Add secret commands Add `signadot secret` command set (create / update / get / list / delete) wrapping the Secrets API from the go-sdk. Values can be supplied via --value, --value-file, --value-stdin, or a flat YAML file with `-f` (with `--set var=val` expansion). Get/list print metadata only, since the API never returns plaintext. Co-Authored-By: Claude Opus 4.7 (1M context) --- go.mod | 30 ++--- go.sum | 76 ++++++------- internal/command/command.go | 2 + internal/command/secret/command.go | 26 +++++ internal/command/secret/create.go | 171 ++++++++++++++++++++++++++++ internal/command/secret/delete.go | 66 +++++++++++ internal/command/secret/get.go | 50 ++++++++ internal/command/secret/list.go | 49 ++++++++ internal/command/secret/printers.go | 45 ++++++++ internal/command/secret/subst.go | 35 ++++++ internal/command/secret/update.go | 65 +++++++++++ internal/config/secret.go | 70 ++++++++++++ 12 files changed, 632 insertions(+), 53 deletions(-) create mode 100644 internal/command/secret/command.go create mode 100644 internal/command/secret/create.go create mode 100644 internal/command/secret/delete.go create mode 100644 internal/command/secret/get.go create mode 100644 internal/command/secret/list.go create mode 100644 internal/command/secret/printers.go create mode 100644 internal/command/secret/subst.go create mode 100644 internal/command/secret/update.go create mode 100644 internal/config/secret.go diff --git a/go.mod b/go.mod index e6b6875..c7adf85 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/docker/go-units v0.5.0 github.com/go-git/go-git/v5 v5.18.0 - github.com/go-openapi/runtime v0.29.3 + github.com/go-openapi/runtime v0.29.4 github.com/go-openapi/strfmt v0.26.1 github.com/goccy/go-yaml v1.10.0 github.com/golang/protobuf v1.5.4 @@ -21,7 +21,7 @@ require ( github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/oklog/run v1.1.0 github.com/panta/machineid v1.0.2 - github.com/signadot/go-sdk v0.3.8-0.20260414192445-2f57b5815443 + github.com/signadot/go-sdk v0.3.8-0.20260422160858-f7db34181fcf github.com/signadot/libconnect v0.1.1-0.20260224205539-f894c2d0a57e github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.11.0 @@ -58,17 +58,17 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.8.0 // indirect - github.com/go-openapi/swag/cmdutils v0.25.5 // indirect - github.com/go-openapi/swag/conv v0.25.5 // indirect - github.com/go-openapi/swag/fileutils v0.25.5 // indirect - github.com/go-openapi/swag/jsonname v0.25.5 // indirect - github.com/go-openapi/swag/jsonutils v0.25.5 // indirect - github.com/go-openapi/swag/loading v0.25.5 // indirect - github.com/go-openapi/swag/mangling v0.25.5 // indirect - github.com/go-openapi/swag/netutils v0.25.5 // indirect - github.com/go-openapi/swag/stringutils v0.25.5 // indirect - github.com/go-openapi/swag/typeutils v0.25.5 // indirect - github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-openapi/swag/cmdutils v0.26.0 // indirect + github.com/go-openapi/swag/conv v0.26.0 // indirect + github.com/go-openapi/swag/fileutils v0.26.0 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect + github.com/go-openapi/swag/jsonutils v0.26.0 // indirect + github.com/go-openapi/swag/loading v0.26.0 // indirect + github.com/go-openapi/swag/mangling v0.26.0 // indirect + github.com/go-openapi/swag/netutils v0.26.0 // indirect + github.com/go-openapi/swag/stringutils v0.26.0 // indirect + github.com/go-openapi/swag/typeutils v0.26.0 // indirect + github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -118,11 +118,11 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.25.0 // indirect github.com/go-openapi/errors v0.22.7 // indirect - github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag v0.26.0 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.7.0 // indirect diff --git a/go.sum b/go.sum index 5f144c5..d74b6a6 100644 --- a/go.sum +++ b/go.sum @@ -158,48 +158,48 @@ github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvC github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= -github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= -github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= -github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= -github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= +github.com/go-openapi/runtime v0.29.4 h1:k2lDxrGoSAJRdhFG2tONKMpkizY/4X1cciSdtzk4Jjo= +github.com/go-openapi/runtime v0.29.4/go.mod h1:K0k/2raY6oqXJnZAgWJB2i/12QKrhUKpZcH4PfV9P18= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= -github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= -github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= -github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= -github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= -github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= -github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= -github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= -github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= -github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= -github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= -github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= -github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= -github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= -github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= -github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= -github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= -github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= -github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= -github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= -github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= -github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= -github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= -github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= -github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= -github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= +github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= +github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -428,8 +428,8 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/signadot/go-sdk v0.3.8-0.20260414192445-2f57b5815443 h1:ctsDTrUSmmucvwzWphBDq1mE7i46WSzlrSGcRzRJun4= -github.com/signadot/go-sdk v0.3.8-0.20260414192445-2f57b5815443/go.mod h1:dOoiOHHKM3oOEVD/WxAIq3Cv37032VfXvQO1IU7jJFk= +github.com/signadot/go-sdk v0.3.8-0.20260422160858-f7db34181fcf h1:b3CJgc9YhOW63KKnRgY4sWqdRtxqC1oeCM/3sx/GQEo= +github.com/signadot/go-sdk v0.3.8-0.20260422160858-f7db34181fcf/go.mod h1:nlOYGZnUJrLxw8lHtEc3GLC0Hos0hcRmtcfT038DW4Y= github.com/signadot/libconnect v0.1.1-0.20260224205539-f894c2d0a57e h1:NiYn5S3cMIhsGh3RzBgRg9NzLDG5qEP7uhSJKtwW7oc= github.com/signadot/libconnect v0.1.1-0.20260224205539-f894c2d0a57e/go.mod h1:cAsgAummH9Q9DrLQ7+S3mqrBv/+ZYKVSEXjR/WfoUJM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -494,8 +494,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= diff --git a/internal/command/command.go b/internal/command/command.go index 25d898e..748814e 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -21,6 +21,7 @@ import ( "github.com/signadot/cli/internal/command/resourceplugin" "github.com/signadot/cli/internal/command/routegroup" "github.com/signadot/cli/internal/command/sandbox" + "github.com/signadot/cli/internal/command/secret" "github.com/signadot/cli/internal/command/smarttest" "github.com/signadot/cli/internal/command/traffic" "github.com/signadot/cli/internal/config" @@ -61,6 +62,7 @@ func New() *cobra.Command { smarttest.New(cfg), traffic.New(cfg), plan.New(cfg), + secret.New(cfg), // hidden commands hostedtest.New(cfg), diff --git a/internal/command/secret/command.go b/internal/command/secret/command.go new file mode 100644 index 0000000..3319a97 --- /dev/null +++ b/internal/command/secret/command.go @@ -0,0 +1,26 @@ +package secret + +import ( + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" +) + +func New(api *config.API) *cobra.Command { + cfg := &config.Secret{API: api} + + cmd := &cobra.Command{ + Use: "secret", + Short: "Manage org-level secrets", + Aliases: []string{"secrets"}, + } + + cmd.AddCommand( + newCreate(cfg), + newUpdate(cfg), + newGet(cfg), + newList(cfg), + newDelete(cfg), + ) + + return cmd +} diff --git a/internal/command/secret/create.go b/internal/command/secret/create.go new file mode 100644 index 0000000..2e4c46a --- /dev/null +++ b/internal/command/secret/create.go @@ -0,0 +1,171 @@ +package secret + +import ( + "errors" + "fmt" + "io" + "os" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + sdksecrets "github.com/signadot/go-sdk/client/secrets" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newCreate(secret *config.Secret) *cobra.Command { + cfg := &config.SecretCreate{Secret: secret} + + cmd := &cobra.Command{ + Use: "create { NAME --value VALUE | NAME --value-file PATH | NAME --value-stdin | -f FILENAME [--set var=val ...] } [--description TEXT]", + Short: "Create a new secret", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return create(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr(), args) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func create(cfg *config.SecretCreate, out, log io.Writer, args []string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + s, err := buildSecretFromInputs(secretInputs{ + Args: args, + Filename: cfg.Filename, + TplVals: cfg.TemplateVals, + Value: cfg.Value, + ValueFile: cfg.ValueFile, + ValueStdin: cfg.ValueStdin, + Description: cfg.Description, + Log: log, + }) + if err != nil { + return err + } + if s.Name == "" { + return errors.New("secret name is required") + } + if s.Value == "" { + return errors.New("value is required; supply one of --value / --value-file / --value-stdin, or a file with -f") + } + + params := sdksecrets.NewCreateSecretParams(). + WithOrgName(cfg.Org). + WithData(s) + resp, err := cfg.Client.Secrets.CreateSecret(params, nil) + if err != nil { + return err + } + + fmt.Fprintf(log, "Created secret %q\n\n", s.Name) + return writeSecretOutput(cfg.OutputFormat, out, resp.Payload) +} + +type secretInputs struct { + Args []string + Filename string + TplVals config.TemplateVals + Value string + ValueFile string + ValueStdin bool + Description string + Log io.Writer +} + +// buildSecretFromInputs resolves a *models.Secret from the combination of a +// positional NAME, value flags, and optional -f file. It enforces mutual +// exclusion between the file mode and the flat-CLI mode. +func buildSecretFromInputs(in secretInputs) (*models.Secret, error) { + if in.Filename != "" { + if len(in.Args) != 0 { + return nil, errors.New("must not provide NAME positional when -f is specified") + } + if in.Value != "" || in.ValueFile != "" || in.ValueStdin { + return nil, errors.New("must not combine -f with --value / --value-file / --value-stdin") + } + if in.Description != "" { + return nil, errors.New("must not combine -f with --description") + } + if len(in.TplVals) != 0 && in.Filename == "" { + return nil, errors.New("--set requires -f") + } + return loadSecretFile(in.Filename, in.TplVals, false /* forDelete */) + } + + if len(in.Args) == 0 { + return nil, errors.New("must specify NAME or -f FILENAME") + } + if len(in.TplVals) != 0 { + return nil, errors.New("--set requires -f") + } + + value, err := resolveValue(in.Value, in.ValueFile, in.ValueStdin, in.Log) + if err != nil { + return nil, err + } + return &models.Secret{ + Name: in.Args[0], + Description: in.Description, + Value: value, + }, nil +} + +// resolveValue reads the secret value from exactly one of the three flag sources. +// Returns "" when none are set; callers decide whether that's an error. +func resolveValue(literal, path string, fromStdin bool, log io.Writer) (string, error) { + n := 0 + if literal != "" { + n++ + } + if path != "" { + n++ + } + if fromStdin { + n++ + } + if n > 1 { + return "", errors.New("--value, --value-file, and --value-stdin are mutually exclusive") + } + + switch { + case literal != "": + fmt.Fprintln(log, "warning: --value leaks the secret into shell history; prefer --value-file or --value-stdin") + return literal, nil + case path != "": + data, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("reading --value-file: %w", err) + } + return string(data), nil + case fromStdin: + fi, err := os.Stdin.Stat() + if err == nil && (fi.Mode()&os.ModeCharDevice) != 0 { + return "", errors.New("--value-stdin was given but stdin is a terminal") + } + data, err := io.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("reading stdin: %w", err) + } + return string(data), nil + default: + return "", nil + } +} + +func writeSecretOutput(format config.OutputFormat, out io.Writer, s *models.Secret) error { + switch format { + case config.OutputFormatDefault: + return nil + case config.OutputFormatJSON: + return print.RawJSON(out, s) + case config.OutputFormatYAML: + return print.RawYAML(out, s) + default: + return fmt.Errorf("unsupported output format: %q", format) + } +} diff --git a/internal/command/secret/delete.go b/internal/command/secret/delete.go new file mode 100644 index 0000000..50fed0d --- /dev/null +++ b/internal/command/secret/delete.go @@ -0,0 +1,66 @@ +package secret + +import ( + "errors" + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + sdksecrets "github.com/signadot/go-sdk/client/secrets" + "github.com/spf13/cobra" +) + +func newDelete(secret *config.Secret) *cobra.Command { + cfg := &config.SecretDelete{Secret: secret} + + cmd := &cobra.Command{ + Use: "delete { NAME | -f FILENAME [--set var=val ...] }", + Short: "Delete a secret", + Aliases: []string{"rm"}, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return deleteSecret(cfg, cmd.ErrOrStderr(), args) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func deleteSecret(cfg *config.SecretDelete, log io.Writer, args []string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + var name string + if cfg.Filename == "" { + if len(args) == 0 { + return errors.New("must specify NAME or -f FILENAME") + } + if len(cfg.TemplateVals) != 0 { + return errors.New("--set requires -f") + } + name = args[0] + } else { + if len(args) != 0 { + return errors.New("must not provide NAME positional when -f is specified") + } + s, err := loadSecretFile(cfg.Filename, cfg.TemplateVals, true /* forDelete */) + if err != nil { + return err + } + name = s.Name + } + if name == "" { + return errors.New("secret name is required") + } + + params := sdksecrets.NewDeleteSecretParams(). + WithOrgName(cfg.Org). + WithSecretName(name) + if _, err := cfg.Client.Secrets.DeleteSecret(params, nil); err != nil { + return err + } + fmt.Fprintf(log, "Deleted secret %q.\n\n", name) + return nil +} diff --git a/internal/command/secret/get.go b/internal/command/secret/get.go new file mode 100644 index 0000000..f03d931 --- /dev/null +++ b/internal/command/secret/get.go @@ -0,0 +1,50 @@ +package secret + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + sdksecrets "github.com/signadot/go-sdk/client/secrets" + "github.com/spf13/cobra" +) + +func newGet(secret *config.Secret) *cobra.Command { + cfg := &config.SecretGet{Secret: secret} + + cmd := &cobra.Command{ + Use: "get NAME", + Short: "Get secret metadata (plaintext value is never returned)", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return get(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + return cmd +} + +func get(cfg *config.SecretGet, out io.Writer, name string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := sdksecrets.NewGetSecretParams(). + WithOrgName(cfg.Org). + WithSecretName(name) + resp, err := cfg.Client.Secrets.GetSecret(params, nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printSecretDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/secret/list.go b/internal/command/secret/list.go new file mode 100644 index 0000000..140772a --- /dev/null +++ b/internal/command/secret/list.go @@ -0,0 +1,49 @@ +package secret + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + sdksecrets "github.com/signadot/go-sdk/client/secrets" + "github.com/spf13/cobra" +) + +func newList(secret *config.Secret) *cobra.Command { + cfg := &config.SecretList{Secret: secret} + + cmd := &cobra.Command{ + Use: "list", + Short: "List secrets (metadata only)", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return list(cfg, cmd.OutOrStdout()) + }, + } + + return cmd +} + +func list(cfg *config.SecretList, out io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := sdksecrets.NewListSecretsParams().WithOrgName(cfg.Org) + resp, err := cfg.Client.Secrets.ListSecrets(params, nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printSecretTable(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/secret/printers.go b/internal/command/secret/printers.go new file mode 100644 index 0000000..aee8fbc --- /dev/null +++ b/internal/command/secret/printers.go @@ -0,0 +1,45 @@ +package secret + +import ( + "fmt" + "io" + "text/tabwriter" + + "github.com/signadot/cli/internal/sdtab" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/models" +) + +type secretRow struct { + Name string `sdtab:"NAME"` + Description string `sdtab:"DESCRIPTION"` + Created string `sdtab:"CREATED"` + Updated string `sdtab:"UPDATED"` +} + +func printSecretTable(out io.Writer, secrets []*models.Secret) error { + t := sdtab.New[secretRow](out) + t.AddHeader() + for _, s := range secrets { + t.AddRow(secretRow{ + Name: s.Name, + Description: s.Description, + Created: s.CreatedAt, + Updated: s.UpdatedAt, + }) + } + return t.Flush() +} + +func printSecretDetails(out io.Writer, s *models.Secret) error { + tw := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + fmt.Fprintf(tw, "Name:\t%s\n", s.Name) + if s.Description != "" { + fmt.Fprintf(tw, "Description:\t%s\n", s.Description) + } + fmt.Fprintf(tw, "Created:\t%s\n", utils.FormatTimestamp(s.CreatedAt)) + if s.UpdatedAt != "" { + fmt.Fprintf(tw, "Updated:\t%s\n", utils.FormatTimestamp(s.UpdatedAt)) + } + return tw.Flush() +} diff --git a/internal/command/secret/subst.go b/internal/command/secret/subst.go new file mode 100644 index 0000000..783840d --- /dev/null +++ b/internal/command/secret/subst.go @@ -0,0 +1,35 @@ +package secret + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/jsonexact" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/models" +) + +// loadSecretFile reads a flat secret YAML/JSON file with `--set` expansion. +// Unlike resource plugins the file has no `spec:` stanza; fields map 1:1 to the Secret model. +// When forDelete is true, only the `name` field is retained after substitution. +func loadSecretFile(file string, tplVals config.TemplateVals, forDelete bool) (*models.Secret, error) { + template, err := utils.LoadUnstructuredTemplate(file, tplVals, forDelete) + if err != nil { + return nil, err + } + if _, ok := template.(map[string]any); !ok { + return nil, fmt.Errorf("secret file must be a YAML/JSON object") + } + d, err := json.Marshal(template) + if err != nil { + return nil, err + } + s := &models.Secret{} + if err := jsonexact.Unmarshal(d, s); err != nil { + return nil, fmt.Errorf("couldn't parse secret file - %s", + strings.TrimPrefix(err.Error(), "json: ")) + } + return s, nil +} diff --git a/internal/command/secret/update.go b/internal/command/secret/update.go new file mode 100644 index 0000000..dae218b --- /dev/null +++ b/internal/command/secret/update.go @@ -0,0 +1,65 @@ +package secret + +import ( + "errors" + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + sdksecrets "github.com/signadot/go-sdk/client/secrets" + "github.com/spf13/cobra" +) + +func newUpdate(secret *config.Secret) *cobra.Command { + cfg := &config.SecretUpdate{Secret: secret} + + cmd := &cobra.Command{ + Use: "update { NAME --value VALUE | NAME --value-file PATH | NAME --value-stdin | -f FILENAME [--set var=val ...] } [--description TEXT]", + Short: "Update an existing secret (value is required)", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return update(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr(), args) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func update(cfg *config.SecretUpdate, out, log io.Writer, args []string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + s, err := buildSecretFromInputs(secretInputs{ + Args: args, + Filename: cfg.Filename, + TplVals: cfg.TemplateVals, + Value: cfg.Value, + ValueFile: cfg.ValueFile, + ValueStdin: cfg.ValueStdin, + Description: cfg.Description, + Log: log, + }) + if err != nil { + return err + } + if s.Name == "" { + return errors.New("secret name is required") + } + if s.Value == "" { + return errors.New("value is required; supply one of --value / --value-file / --value-stdin, or a file with -f") + } + + params := sdksecrets.NewUpdateSecretParams(). + WithOrgName(cfg.Org). + WithSecretName(s.Name). + WithData(s) + resp, err := cfg.Client.Secrets.UpdateSecret(params, nil) + if err != nil { + return err + } + + fmt.Fprintf(log, "Updated secret %q\n\n", s.Name) + return writeSecretOutput(cfg.OutputFormat, out, resp.Payload) +} diff --git a/internal/config/secret.go b/internal/config/secret.go new file mode 100644 index 0000000..f17c526 --- /dev/null +++ b/internal/config/secret.go @@ -0,0 +1,70 @@ +package config + +import "github.com/spf13/cobra" + +type Secret struct { + *API +} + +type SecretCreate struct { + *Secret + + // Flags + Value string + ValueFile string + ValueStdin bool + Description string + Filename string + TemplateVals TemplateVals +} + +func (c *SecretCreate) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Value, "value", "", "secret value as a literal string (leaks into shell history)") + cmd.Flags().StringVar(&c.ValueFile, "value-file", "", "path to a file whose contents become the secret value") + cmd.Flags().BoolVar(&c.ValueStdin, "value-stdin", false, "read the secret value from stdin") + cmd.Flags().StringVar(&c.Description, "description", "", "human-readable description") + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "YAML or JSON file containing the secret (fields: name, value, description)") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val (used with -f)") +} + +type SecretUpdate struct { + *Secret + + // Flags + Value string + ValueFile string + ValueStdin bool + Description string + Filename string + TemplateVals TemplateVals +} + +func (c *SecretUpdate) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Value, "value", "", "new secret value as a literal string (leaks into shell history)") + cmd.Flags().StringVar(&c.ValueFile, "value-file", "", "path to a file whose contents become the new secret value") + cmd.Flags().BoolVar(&c.ValueStdin, "value-stdin", false, "read the new secret value from stdin") + cmd.Flags().StringVar(&c.Description, "description", "", "new human-readable description") + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "YAML or JSON file containing the secret (fields: name, value, description)") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val (used with -f)") +} + +type SecretGet struct { + *Secret +} + +type SecretList struct { + *Secret +} + +type SecretDelete struct { + *Secret + + // Flags + Filename string + TemplateVals TemplateVals +} + +func (c *SecretDelete) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "optional YAML or JSON file containing the original secret (name is read from it)") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") +} From 03a0949eead55ae00a32e32add0b0dd6c5f2e610 Mon Sep 17 00:00:00 2001 From: Daniel De Vera Date: Wed, 29 Apr 2026 09:57:45 -0300 Subject: [PATCH 2/2] Document secret commands in agent skill Add a Secrets section to agent-skills/signadot-cli/SKILL.md so agents loading the skill discover the new `signadot secret` commands and the plan run --param-secret binding. Also list secret/secrets in the skill description and argument-hint. Co-Authored-By: Claude Opus 4.7 (1M context) --- agent-skills/signadot-cli/SKILL.md | 48 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/agent-skills/signadot-cli/SKILL.md b/agent-skills/signadot-cli/SKILL.md index d198ddd..ed0c07b 100644 --- a/agent-skills/signadot-cli/SKILL.md +++ b/agent-skills/signadot-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: signadot-cli -description: Manage Signadot sandboxes, route groups, clusters, resource plugins, jobs, and smart tests using the signadot CLI. Use when a developer or platform engineer needs to create, update, inspect, or delete Signadot resources. -argument-hint: "[resource: sandbox|routegroup|cluster|job|resourceplugin|smart-test]" +description: Manage Signadot sandboxes, route groups, clusters, resource plugins, secrets, jobs, and smart tests using the signadot CLI. Use when a developer or platform engineer needs to create, update, inspect, or delete Signadot resources. +argument-hint: "[resource: sandbox|routegroup|cluster|job|resourceplugin|secret|smart-test]" --- # Signadot CLI @@ -272,6 +272,50 @@ signadot resourceplugin get my-plugin signadot resourceplugin delete my-plugin ``` +## Secrets (alias: secrets) + +Org-level encrypted secrets. The plaintext value is **write-only** — `get`/`list` return metadata only (`name`, `description`, `createdAt`, `updatedAt`) and never expose the value. + +```bash +# Create — value can come from a literal, a file, or stdin +signadot secret create my-db-password --value 'hunter2' +signadot secret create my-db-password --value-file ./password.txt +echo -n 'hunter2' | signadot secret create my-db-password --value-stdin + +# Or from a flat YAML/JSON file with optional --set expansion +signadot secret create -f secret.yaml --set VALUE=hunter2 + +# Update — value is required (same flag set as create) +signadot secret update my-db-password --value 'newpass' + +# Inspect (metadata only) +signadot secret get my-db-password +signadot secret list +signadot secret list -o json + +# Delete +signadot secret delete my-db-password +signadot secret delete -f secret.yaml +``` + +Secret file shape (no `spec:` stanza): + +```yaml +name: my-db-password +description: Prod DB password +value: '@{VALUE}' +``` + +### Binding secrets to plan params + +Plan runs pull a secret value into a parameter via `--param-secret param-name=secret-name` (parallel to `--param`, repeatable): + +```bash +signadot plan run my-plan --param-secret db_pass=my-db-password +``` + +A given param name must use either `--param` or `--param-secret`, not both. + ## Job Management Jobs run tests or tasks in the context of a sandbox.