diff --git a/go.work.sum b/go.work.sum index 1e3b7a1a4..927ba0f41 100644 --- a/go.work.sum +++ b/go.work.sum @@ -4,6 +4,7 @@ cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= cloud.google.com/go/accessapproval v1.7.5 h1:uzmAMSgYcnlHa9X9YSQZ4Q1wlfl4NNkZyQgho1Z6p04= cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0= @@ -78,9 +79,11 @@ cloud.google.com/go/assuredworkloads v1.11.6 h1:3NlUes0xLN2kcSU24qQADFYsOaetCPg0 cloud.google.com/go/assuredworkloads v1.11.6/go.mod h1:1dlhWKocQorGYkspt+scx11kQCI9qVHOi1Au6Rw9srg= cloud.google.com/go/assuredworkloads v1.12.1 h1:B+hWc62fYL8NdntPjx0rzJJ67qx99w6dCeIVDpHf7QE= cloud.google.com/go/assuredworkloads v1.12.1/go.mod h1:nBnkK2GZNSdtjU3ER75oC5fikub5/+QchbolKgnMI/I= +cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= cloud.google.com/go/auth v0.9.4/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA= cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/automl v1.13.5 h1:ijiJy9sYWh75WrqImXsfWc1e3HR3iO+ef9fvW03Ig/4= cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm9OJ3xJ1Y= @@ -1099,9 +1102,11 @@ github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/cloud-bigtable-clients-test v0.0.2 h1:S+sCHWAiAc+urcEnvg5JYJUOdlQEm/SEzQ/c/IdAH5M= github.com/googleapis/cloud-bigtable-clients-test v0.0.2/go.mod h1:mk3CrkrouRgtnhID6UZQDK3DrFFa7cYCAJcEmNsHYrY= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/gax-go/v2 v2.12.1/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= @@ -1188,6 +1193,10 @@ github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVET github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= +github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= +github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -1567,6 +1576,7 @@ github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18W github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty v1.13.1/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= @@ -1599,26 +1609,31 @@ go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwf go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -1639,6 +1654,7 @@ golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+ golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= @@ -1648,6 +1664,7 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= @@ -1655,9 +1672,11 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= @@ -1665,8 +1684,11 @@ golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2 golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1680,20 +1702,25 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1768,6 +1795,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= @@ -1788,10 +1816,11 @@ google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.27 h1:kJdccidYzt3CaHD1crCFTS1hxyhSi059NhOFUf03YFo= diff --git a/taco/.gitignore b/taco/.gitignore index 491c71954..ee9e12565 100644 --- a/taco/.gitignore +++ b/taco/.gitignore @@ -1,3 +1,2 @@ -taco statesman terraform-provider-opentaco diff --git a/taco/internal/api/routes.go b/taco/internal/api/routes.go index 629766a77..34d44b31b 100644 --- a/taco/internal/api/routes.go +++ b/taco/internal/api/routes.go @@ -1,26 +1,26 @@ package api import ( - "context" - "fmt" - "log" - "net/http" - "time" - - "github.com/diggerhq/digger/opentaco/internal/analytics" - "github.com/diggerhq/digger/opentaco/internal/tfe" - - "github.com/diggerhq/digger/opentaco/internal/backend" - authpkg "github.com/diggerhq/digger/opentaco/internal/auth" - "github.com/diggerhq/digger/opentaco/internal/middleware" - "github.com/diggerhq/digger/opentaco/internal/rbac" - "github.com/diggerhq/digger/opentaco/internal/s3compat" - unithandlers "github.com/diggerhq/digger/opentaco/internal/unit" - "github.com/diggerhq/digger/opentaco/internal/observability" - "github.com/diggerhq/digger/opentaco/internal/oidc" - "github.com/diggerhq/digger/opentaco/internal/sts" - "github.com/diggerhq/digger/opentaco/internal/storage" - "github.com/labstack/echo/v4" + "context" + "fmt" + "log" + "net/http" + "time" + + "github.com/diggerhq/digger/opentaco/internal/analytics" + "github.com/diggerhq/digger/opentaco/internal/tfe" + + authpkg "github.com/diggerhq/digger/opentaco/internal/auth" + "github.com/diggerhq/digger/opentaco/internal/backend" + "github.com/diggerhq/digger/opentaco/internal/middleware" + "github.com/diggerhq/digger/opentaco/internal/observability" + "github.com/diggerhq/digger/opentaco/internal/oidc" + "github.com/diggerhq/digger/opentaco/internal/rbac" + "github.com/diggerhq/digger/opentaco/internal/s3compat" + "github.com/diggerhq/digger/opentaco/internal/storage" + "github.com/diggerhq/digger/opentaco/internal/sts" + unithandlers "github.com/diggerhq/digger/opentaco/internal/unit" + "github.com/labstack/echo/v4" ) // RegisterRoutes registers all API routes @@ -29,7 +29,7 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { health := observability.NewHealthHandler() e.GET("/healthz", health.Healthz) e.GET("/readyz", health.Readyz) - + // Info endpoint for CLI to detect storage type e.GET("/v1/info", func(c echo.Context) error { info := map[string]interface{}{ @@ -37,7 +37,7 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { "type": "memory", }, } - + // Check if we're using S3 storage if s3Store, ok := store.(storage.S3Store); ok { info["storage"] = map[string]interface{}{ @@ -46,11 +46,10 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { "prefix": s3Store.GetS3Prefix(), } } - + return c.JSON(http.StatusOK, info) }) - // Prepare auth deps signer, err := authpkg.NewSignerFromEnv() if err != nil { @@ -84,7 +83,7 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { } return c.String(http.StatusOK, email) }) - + e.POST("/v1/system-id/user-email", func(c echo.Context) error { var req struct { Email string `json:"email"` @@ -92,15 +91,15 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"}) } - + // Set user email in analytics system ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - + if err := analytics.SetUserEmail(ctx, req.Email); err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to set email"}) } - + return c.JSON(http.StatusOK, map[string]string{"message": "Email set successfully"}) }) @@ -111,7 +110,6 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { e.GET("/oauth/oidc-callback", authHandler.OAuthOIDCCallback) e.GET("/oauth/debug", authHandler.DebugConfig) - // API v1 protected group - JWT tokens only v1 := e.Group("/v1") if authEnabled { @@ -170,7 +168,7 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { v1.GET("/backend/*", middleware.JWTOnlyRBACMiddleware(rbacManager, signer, rbac.ActionUnitRead, "*")(backendHandler.GetState)) v1.POST("/backend/*", middleware.JWTOnlyRBACMiddleware(rbacManager, signer, rbac.ActionUnitWrite, "*")(backendHandler.UpdateState)) v1.PUT("/backend/*", middleware.JWTOnlyRBACMiddleware(rbacManager, signer, rbac.ActionUnitWrite, "*")(backendHandler.UpdateState)) - // Explicitly wire non-standard HTTP methods used by Terraform backend + // Explicitly wire non-standard HTTP methods used by Terraform backend jwtVerifyFn := middleware.JWTOnlyVerifier(signer) e.Add("LOCK", "/v1/backend/*", middleware.RequireAuth(jwtVerifyFn)(middleware.JWTOnlyRBACMiddleware(rbacManager, signer, rbac.ActionUnitLock, "*")(backendHandler.HandleLockUnlock))) e.Add("UNLOCK", "/v1/backend/*", middleware.RequireAuth(jwtVerifyFn)(middleware.JWTOnlyRBACMiddleware(rbacManager, signer, rbac.ActionUnitLock, "*")(backendHandler.HandleLockUnlock))) @@ -200,13 +198,13 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { // RBAC routes (only available with S3 storage) if rbacManager != nil { rbacHandler := rbac.NewHandler(rbacManager, signer) - + // RBAC initialization (no auth required for init) v1.POST("/rbac/init", rbacHandler.Init) - + // RBAC user info (handle auth gracefully in handler, like /v1/auth/me) e.GET("/v1/rbac/me", rbacHandler.Me) - + // RBAC management routes (require RBAC manage permission) v1.POST("/rbac/users/assign", rbacHandler.AssignRole) v1.POST("/rbac/users/revoke", rbacHandler.RevokeRole) @@ -224,14 +222,14 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { // RBAC not available with memory storage - add catch-all route v1.Any("/rbac/*", func(c echo.Context) error { return c.JSON(http.StatusBadRequest, map[string]string{ - "error": "RBAC requires S3 storage", + "error": "RBAC requires S3 storage", "message": "RBAC is only available when using S3 storage. Please configure S3 storage to use RBAC features.", }) }) } // TFE api - inject auth handler, storage, and RBAC dependencies - tfeHandler := tfe.NewTFETokenHandler(authHandler, store, rbacManager) // Pass rbacManager (may be nil) + tfeHandler := tfe.NewTFETokenHandler(authHandler, store, rbacManager) // Pass rbacManager (may be nil) // Create protected TFE group - opaque tokens only tfeGroup := e.Group("/tfe/api/v2") @@ -261,6 +259,8 @@ func RegisterRoutes(e *echo.Echo, store storage.UnitStore, authEnabled bool) { // Keep discovery endpoints unprotected (needed for terraform login) e.GET("/.well-known/terraform.json", tfeHandler.GetWellKnownJson) + e.GET("/tfe/api/v2/motd", tfeHandler.MessageOfTheDay) + e.GET("/tfe/app/oauth2/auth", tfeHandler.AuthLogin) e.POST("/tfe/oauth2/token", tfeHandler.AuthTokenExchange) diff --git a/taco/internal/domain/tfe_id.go b/taco/internal/domain/tfe_id.go deleted file mode 100644 index 3923dc70f..000000000 --- a/taco/internal/domain/tfe_id.go +++ /dev/null @@ -1,124 +0,0 @@ -package domain - -import ( - "database/sql/driver" - "fmt" - "math/rand" - "strings" -) - -const base58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type ID interface { - fmt.Stringer - Kind() Kind -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -func GenerateRandomStringFromAlphabet(size int, alphabet string) string { - buf := make([]byte, size) - for i := 0; i < size; i++ { - buf[i] = alphabet[rand.Intn(len(alphabet))] - } - return string(buf) -} - -// ConvertTfeID converts an ID for use with a different resource kind, e.g. convert -// run-123 to plan-123. -func ConvertTfeID(id TfeID, to Kind) TfeID { - return TfeID{kind: to, id: id.id} -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -func MustHardcodeTfeID(kind Kind, suffix string) TfeID { - s := fmt.Sprintf("%s-%s", kind, suffix) - id, err := ParseTfeID(s) - if err != nil { - panic("failed to parse hardcoded ID: " + err.Error()) - } - return id -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -// ParseTfeID parses the ID from a string representation. -func ParseTfeID(s string) (TfeID, error) { - parts := strings.Split(s, "-") - if len(parts) != 2 { - return TfeID{}, fmt.Errorf("malformed ID: %s", s) - } - kind := parts[0] - if len(kind) < 2 { - return TfeID{}, fmt.Errorf("kind must be at least 2 characters: %s", s) - } - id := parts[1] - if len(id) < 1 { - return TfeID{}, fmt.Errorf("id suffix must be at least 1 character: %s", s) - } - return TfeID{kind: Kind(kind), id: id}, nil -} - -// The following struct and all struct methods have been -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TfeID struct { - kind Kind - id string -} - -func (id TfeID) String() string { - return fmt.Sprintf("%s-%s", id.kind, id.id) -} - -func (id TfeID) Kind() Kind { - return id.kind -} - -func (id TfeID) MarshalText() ([]byte, error) { - return []byte(id.String()), nil -} - -func (id *TfeID) UnmarshalText(text []byte) error { - if len(text) == 0 { - return nil - } - s := string(text) - x, err := ParseTfeID(s) - if err != nil { - return err - } - *id = x - return nil -} - -func (id *TfeID) Scan(text any) error { - if text == nil { - return nil - } - s, ok := text.(string) - if !ok { - return fmt.Errorf("expected database value to be a string: %#v", text) - } - x, err := ParseTfeID(s) - if err != nil { - return err - } - *id = x - return nil -} - -func (id *TfeID) Value() (driver.Value, error) { - if id == nil { - return nil, nil - } - return id.String(), nil -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -func NewTfeID(kind Kind) TfeID { - return TfeID{kind: kind, id: GenerateRandomStringFromAlphabet(16, base58)} -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -func NewTfeIDWithVal(kind Kind, id string) TfeID { - return TfeID{kind: kind, id: id} -} diff --git a/taco/internal/domain/tfe_kind.go b/taco/internal/domain/tfe_kind.go deleted file mode 100644 index f3fd95574..000000000 --- a/taco/internal/domain/tfe_kind.go +++ /dev/null @@ -1,36 +0,0 @@ -package domain - -type Kind string - -func (k Kind) String() string { - return string(k) -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -const ( - SiteKind Kind = "site" - OrganizationKind Kind = "org" - WorkspaceKind Kind = "ws" - RunKind Kind = "run" - ConfigVersionKind Kind = "cv" - IngressAttributesKind Kind = "ia" - JobKind Kind = "job" - ChunkKind Kind = "chunk" - UserKind Kind = "user" - TeamKind Kind = "team" - ModuleKind Kind = "mod" - ModuleVersionKind Kind = "modver" - NotificationConfigurationKind Kind = "nc" - AgentPoolKind Kind = "apool" - RunnerKind Kind = "runner" - StateVersionKind Kind = "sv" - StateVersionOutputKind Kind = "wsout" - VariableSetKind Kind = "varset" - VariableKind Kind = "var" - VCSProviderKind Kind = "vcs" - - OrganizationTokenKind Kind = "ot" - UserTokenKind Kind = "ut" - TeamTokenKind Kind = "tt" - AgentTokenKind Kind = "at" -) diff --git a/taco/internal/domain/tfe_org.go b/taco/internal/domain/tfe_org.go deleted file mode 100644 index d9ff4fa0e..000000000 --- a/taco/internal/domain/tfe_org.go +++ /dev/null @@ -1,77 +0,0 @@ -package domain - -import "time" - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -var DefaultOrganizationPermissions = TFEOrganizationPermissions{ - CanCreateWorkspace: true, - CanUpdate: true, - CanDestroy: true, -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFEOrganizationPermissions struct { - CanCreateTeam bool `json:"can-create-team"` - CanCreateWorkspace bool `json:"can-create-workspace"` - CanCreateWorkspaceMigration bool `json:"can-create-workspace-migration"` - CanDestroy bool `json:"can-destroy"` - CanTraverse bool `json:"can-traverse"` - CanUpdate bool `json:"can-update"` - CanUpdateAPIToken bool `json:"can-update-api-token"` - CanUpdateOAuth bool `json:"can-update-oauth"` - CanUpdateSentinel bool `json:"can-update-sentinel"` -} - -type Name struct { - Name string -} - -func NewName(name string) Name { - return Name{Name: name} -} - -type TFEAuthPolicyType string - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFEOrganization struct { - // Primary ID - Name string `jsonapi:"primary,organizations"` - - // Attributes - AssessmentsEnforced bool `jsonapi:"attr,assessments-enforced" json:"assessments-enforced"` - CollaboratorAuthPolicy TFEAuthPolicyType `jsonapi:"attr,collaborator-auth-policy" json:"collaborator-auth-policy"` - CostEstimationEnabled bool `jsonapi:"attr,cost-estimation-enabled" json:"cost-estimation-enabled"` - CreatedAt time.Time `jsonapi:"attr,created-at" json:"created-at"` - Email string `jsonapi:"attr,email" json:"email"` - ExternalID string `jsonapi:"attr,external-id" json:"external-id"` - OwnersTeamSAMLRoleID string `jsonapi:"attr,owners-team-saml-role-id" json:"owners-team-saml-role-id"` - Permissions *TFEOrganizationPermissions `jsonapi:"attr,permissions" json:"permissions"` - SAMLEnabled bool `jsonapi:"attr,saml-enabled" json:"saml-enabled"` - SessionRemember *int `jsonapi:"attr,session-remember" json:"session-remember"` - SessionTimeout *int `jsonapi:"attr,session-timeout" json:"session-timeout"` - TrialExpiresAt time.Time `jsonapi:"attr,trial-expires-at" json:"trial-expires-at"` - TwoFactorConformant bool `jsonapi:"attr,two-factor-conformant" json:"two-factor-conformant"` - SendPassingStatusesForUntriggeredSpeculativePlans bool `jsonapi:"attr,send-passing-statuses-for-untriggered-speculative-plans" json:"send-passing-statuses-for-untriggered-speculative-plans"` - RemainingTestableCount int `jsonapi:"attr,remaining-testable-count" json:"remaining-testable-count"` - - // Note: false on TFE < v202211 where setting doesn’t exist (all deletes are force). - AllowForceDeleteWorkspaces bool `jsonapi:"attr,allow-force-delete-workspaces" json:"allow-force-delete-workspaces"` - - // Relations - // DefaultProject *Project `jsonapi:"relation,default-project"` -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFEEntitlements struct { - ID string `jsonapi:"primary,entitlement-sets"` - Agents bool `jsonapi:"attr,agents" json:"agents"` - AuditLogging bool `jsonapi:"attr,audit-logging" json:"audit-logging"` - CostEstimation bool `jsonapi:"attr,cost-estimation" json:"cost-estimation"` - Operations bool `jsonapi:"attr,operations" json:"operations"` - PrivateModuleRegistry bool `jsonapi:"attr,private-module-registry" json:"private-module-registry"` - SSO bool `jsonapi:"attr,sso" json:"sso"` - Sentinel bool `jsonapi:"attr,sentinel" json:"sentinel"` - StateStorage bool `jsonapi:"attr,state-storage" json:"state-storage"` - Teams bool `jsonapi:"attr,teams" json:"teams"` - VCSIntegrations bool `jsonapi:"attr,vcs-integrations" json:"vcs-integrations"` -} diff --git a/taco/internal/domain/tfe_workspace.go b/taco/internal/domain/tfe_workspace.go deleted file mode 100644 index 8a518927b..000000000 --- a/taco/internal/domain/tfe_workspace.go +++ /dev/null @@ -1,184 +0,0 @@ -package domain - -import "time" - -type ExecutionMode string - -type RunStatus string - -type LatestRun struct { - ID string - Status RunStatus -} - -type VCSRepo struct { - owner string - name string -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type VCSConnection struct { - // Pushes to this VCS branch trigger runs. Empty string means the default - // branch is used. Ignored if TagsRegex is non-empty. - Branch string - // Pushed tags matching this regular expression trigger runs. Mutually - // exclusive with TriggerPatterns. - TagsRegex string - - VCSProviderID string - Repo VCSRepo - - // By default, once a workspace is connected to a repo it is not - // possible to run a terraform apply via the CLI. Setting this to true - // overrides this behaviour. - AllowCLIApply bool -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type WorkspaceVersion struct { - // Latest if true means runs use the Latest available version at time of - // creation of the run. - Latest bool - // semver is the semantic version of the engine; must be non-empty if latest - // is false. - // - // TODO: use custom type - semver string -} - -// Following struct and its methods have been -// Adapted from OTF (MPL License): https://github.com/leg100/otf -// If you ever marshal this domain type via jsonapi (you currently don't), -// its tags must also be valid. Fixing them anyway for completeness. -type Workspace struct { - ID string `jsonapi:"primary,workspaces"` - CreatedAt time.Time `jsonapi:"attr,created_at" json:"created_at"` - UpdatedAt time.Time `jsonapi:"attr,updated_at" json:"updated_at"` - AgentPoolID string `jsonapi:"attr,agent-pool-id" json:"agent-pool-id"` - AllowDestroyPlan bool `jsonapi:"attr,allow_destroy_plan" json:"allow_destroy_plan"` - AutoApply bool `jsonapi:"attr,auto_apply" json:"auto_apply"` - CanQueueDestroyPlan bool `jsonapi:"attr,can_queue_destroy_plan" json:"can_queue_destroy_plan"` - Description string `jsonapi:"attr,description" json:"description"` - Environment string `jsonapi:"attr,environment" json:"environment"` - ExecutionMode ExecutionMode `jsonapi:"attr,execution_mode" json:"execution_mode"` - GlobalRemoteState bool `jsonapi:"attr,global_remote_state" json:"global_remote_state"` - MigrationEnvironment string `jsonapi:"attr,migration_environment" json:"migration_environment"` - Name Name `jsonapi:"attr,Name" json:"Name"` - QueueAllRuns bool `jsonapi:"attr,queue_all_runs" json:"queue_all_runs"` - SpeculativeEnabled bool `jsonapi:"attr,speculative_enabled" json:"speculative_enabled"` - StructuredRunOutputEnabled bool `jsonapi:"attr,structured_run_output_enabled" json:"structured_run_output_enabled"` - SourceName string `jsonapi:"attr,source_name" json:"source_name"` - SourceURL string `jsonapi:"attr,source_url" json:"source_url"` - WorkingDirectory string `jsonapi:"attr,working_directory" json:"working_directory"` - Organization Name `jsonapi:"attr,organization" json:"organization"` - LatestRun *LatestRun `jsonapi:"attr,latest_run" json:"latest_run"` - Tags []string `jsonapi:"attr,tags" json:"tags"` - Lock ID `json:"-"` - Engine string `jsonapi:"attr,engine" json:"engine"` - EngineVersion *WorkspaceVersion `jsonapi:"attr,engine_version" json:"engine_version"` - - // VCS Connection; nil means the workspace is not connected. - Connection *VCSConnection - - // TriggerPatterns is mutually exclusive with Connection.TagsRegex. - TriggerPatterns []string `jsonapi:"attr,trigger-patterns" json:"trigger_patterns"` - - // Exists only to satisfy go-tfe tests. - TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes" json:"trigger_prefixes"` -} - -func (ws *Workspace) Locked() bool { - return ws.Lock != nil // assuming ID is a pointer-like alias; keep your original semantics -} - -// ---- JSON:API DTOs below ---- - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFEWorkspaceActions struct { - IsDestroyable bool `json:"is-destroyable"` -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFEWorkspacePermissions struct { - CanDestroy bool `json:"can-destroy"` - CanForceUnlock bool `json:"can-force-unlock"` - CanLock bool `json:"can-lock"` - CanQueueApply bool `json:"can-queue-apply"` - CanQueueDestroy bool `json:"can-queue-destroy"` - CanQueueRun bool `json:"can-queue-run"` - CanReadSettings bool `json:"can-read-settings"` - CanUnlock bool `json:"can-unlock"` - CanUpdate bool `json:"can-update"` - CanUpdateVariable bool `json:"can-update-variable"` -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -// TFEVCSRepo is carried as a single attribute object on the workspace. -type TFEVCSRepo struct { - Branch string `json:"branch"` - DisplayIdentifier string `json:"display-identifier"` - Identifier VCSRepo `json:"identifier"` - IngressSubmodules bool `json:"ingress-submodules"` - OAuthTokenID string `json:"oauth-token-id"` - RepositoryHTTPURL string `json:"repository-http-url"` - TagsRegex string `json:"tags-regex"` - ServiceProvider string `json:"service-provider"` -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFERun struct { - ID string `jsonapi:"primary,runs"` -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFEWorkspace struct { - ID string `jsonapi:"primary,workspaces"` - Actions *TFEWorkspaceActions `jsonapi:"attr,actions" json:"actions"` - AgentPoolID string `jsonapi:"attr,agent-pool-id" json:"agent-pool-id"` - AllowDestroyPlan bool `jsonapi:"attr,allow-destroy-plan" json:"allow-destroy-plan"` - AutoApply bool `jsonapi:"attr,auto-apply" json:"auto-apply"` - CanQueueDestroyPlan bool `jsonapi:"attr,can-queue-destroy-plan" json:"can-queue-destroy-plan"` - CreatedAt time.Time `jsonapi:"attr,created-at" json:"created-at"` - Description string `jsonapi:"attr,description" json:"description"` - Environment string `jsonapi:"attr,environment" json:"environment"` - ExecutionMode string `jsonapi:"attr,execution-mode" json:"execution-mode"` - FileTriggersEnabled bool `jsonapi:"attr,file-triggers-enabled" json:"file-triggers-enabled"` - GlobalRemoteState bool `jsonapi:"attr,global-remote-state" json:"global-remote-state"` - Locked bool `jsonapi:"attr,locked" json:"locked"` - MigrationEnvironment string `jsonapi:"attr,migration-environment" json:"migration-environment"` - Name string `jsonapi:"attr,Name" json:"Name"` - Operations bool `jsonapi:"attr,operations" json:"operations"` - Permissions *TFEWorkspacePermissions `jsonapi:"attr,permissions" json:"permissions"` - QueueAllRuns bool `jsonapi:"attr,queue-all-runs" json:"queue-all-runs"` - SpeculativeEnabled bool `jsonapi:"attr,speculative-enabled" json:"speculative-enabled"` - SourceName string `jsonapi:"attr,source-Name" json:"source-Name"` - SourceURL string `jsonapi:"attr,source-url" json:"source-url"` - StructuredRunOutputEnabled bool `jsonapi:"attr,structured-run-output-enabled" json:"structured-run-output-enabled"` - TerraformVersion *WorkspaceVersion `jsonapi:"attr,terraform-version" json:"terraform-version"` - TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes" json:"trigger-prefixes"` - TriggerPatterns []string `jsonapi:"attr,trigger-patterns" json:"trigger-patterns"` - VCSRepo *TFEVCSRepo `jsonapi:"attr,vcs-repo" json:"vcs-repo"` - WorkingDirectory string `jsonapi:"attr,working-directory" json:"working-directory"` - UpdatedAt time.Time `jsonapi:"attr,updated-at" json:"updated-at"` - ResourceCount int `jsonapi:"attr,resource-count" json:"resource-count"` - ApplyDurationAverage time.Duration `jsonapi:"attr,apply-duration-average" json:"apply-duration-average"` - PlanDurationAverage time.Duration `jsonapi:"attr,plan-duration-average" json:"plan-duration-average"` - PolicyCheckFailures int `jsonapi:"attr,policy-check-failures" json:"policy-check-failures"` - RunFailures int `jsonapi:"attr,run-failures" json:"run-failures"` - RunsCount int `jsonapi:"attr,workspace-kpis-runs-count" json:"workspace-kpis-runs-count"` - TagNames []string `jsonapi:"attr,tag-names" json:"tag-names"` - - // Relations - CurrentRun *TFERun `jsonapi:"relation,current-run" json:"current-run"` - Organization *TFEOrganization `jsonapi:"relation,organization" json:"organization"` - Outputs []*TFEWorkspaceOutput `jsonapi:"relation,outputs" json:"outputs"` -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type TFEWorkspaceOutput struct { - ID string `jsonapi:"primary,workspace-outputs"` - Name string `jsonapi:"attr,Name" json:"Name"` - Sensitive bool `jsonapi:"attr,sensitive" json:"sensitive"` - Type string `jsonapi:"attr,output-type" json:"output-type"` - Value any `jsonapi:"attr,value" json:"value"` -} diff --git a/taco/internal/tfe/organizations.go b/taco/internal/tfe/organizations.go index 64f675343..4a660c3ef 100644 --- a/taco/internal/tfe/organizations.go +++ b/taco/internal/tfe/organizations.go @@ -1,67 +1,18 @@ package tfe import ( + "github.com/diggerhq/digger/opentaco/internal/domain/tfe" "github.com/google/jsonapi" ) import ( "fmt" - "github.com/diggerhq/digger/opentaco/internal/domain" "github.com/labstack/echo/v4" ) -// Adapted from OTF (MPL License): https://github.com/leg100/otf -type Entitlements struct { - ID domain.TfeID - Agents bool - AuditLogging bool - CostEstimation bool - Operations bool - PrivateModuleRegistry bool - SSO bool - Sentinel bool - StateStorage bool - Teams bool - VCSIntegrations bool -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf -func defaultEntitlements(organizationID domain.TfeID) Entitlements { - return Entitlements{ - ID: organizationID, - Agents: true, - AuditLogging: true, - CostEstimation: true, - Operations: true, - PrivateModuleRegistry: true, - SSO: true, - Sentinel: true, - StateStorage: true, - Teams: true, - VCSIntegrations: true, - } -} - -// Adapted from OTF (MPL License): https://github.com/leg100/otf func (h *TfeHandler) GetOrganizationEntitlements(c echo.Context) error { - tfidStr := domain.NewTfeIDWithVal(domain.OrganizationKind, "RoiPNhWzpjaKhjcV") - domain.NewTfeID(domain.OrganizationKind) - ents := defaultEntitlements(tfidStr) - - // map to the JSON:API DTO - payload := &domain.TFEEntitlements{ - ID: ents.ID.String(), // same concrete type domain.TfeID - Agents: ents.Agents, - AuditLogging: ents.AuditLogging, - CostEstimation: ents.CostEstimation, - Operations: ents.Operations, - PrivateModuleRegistry: ents.PrivateModuleRegistry, - SSO: ents.SSO, - Sentinel: ents.Sentinel, - StateStorage: ents.StateStorage, - Teams: ents.Teams, - VCSIntegrations: ents.VCSIntegrations, - } + tfidStr := tfe.NewTfeResourceIdentifier(tfe.OrganizationType, "RoiPNhWzpjaKhjcV") + payload := tfe.DefaultFeatureEntitlements(tfidStr.String()) c.Response().Header().Set(echo.HeaderContentType, "application/vnd.api+json") c.Response().Header().Set("Tfp-Api-Version", "2.5") diff --git a/taco/internal/tfe/well_known.go b/taco/internal/tfe/well_known.go index 893a26be1..d73e87772 100644 --- a/taco/internal/tfe/well_known.go +++ b/taco/internal/tfe/well_known.go @@ -1,8 +1,9 @@ package tfe import ( - "os" + "github.com/diggerhq/digger/opentaco/internal/domain/tfe" "github.com/labstack/echo/v4" + "os" ) const ( @@ -12,52 +13,13 @@ const ( ModuleV1Prefix = "/v1/modules/" ) -const ( - // OAuth2 client ID - purely advisory according to: - // https://developer.hashicorp.com/terraform/internals/v1.3.x/login-protocol#client - ClientID = "terraform" - - AuthRoute = "/tfe/app/oauth2/auth" - TokenRoute = "/tfe/oauth2/token" -) - -// These Discovery structs have been -// Adapted from OTF (MPL License): https://github.com/leg100/otf -// login stuff, TODO: move to own package etc -type DiscoverySpec struct { - Client string `json:"client"` - GrantTypes []string `json:"grant_types"` - Authz string `json:"authz"` - Token string `json:"token"` - Ports []int `json:"ports"` -} - -var Discovery = DiscoverySpec{ - Client: ClientID, - GrantTypes: []string{"authz_code"}, - Authz: AuthRoute, - Token: TokenRoute, - Ports: []int{10000, 10010}, -} - -type DiscoveryDef struct { - ModulesV1 string `json:"modules.v1"` - MotdV1 string `json:"motd.v1"` - StateV2 string `json:"state.v2"` - TfeV2 string `json:"tfe.v2"` - TfeV21 string `json:"tfe.v2.1"` - TfeV22 string `json:"tfe.v2.2"` - LoginV1 DiscoverySpec `json:"login.v1"` -} +func (h *TfeHandler) MessageOfTheDay(c echo.Context) error { + c.Response().Header().Set(echo.HeaderContentType, "application/json") + c.Response().Header().Set("Tfp-Api-Version", "2.5") + c.Response().Header().Set("X-Terraform-Enterprise-App", "Terraform Enterprise") -var discoveryPayload = DiscoveryDef{ - ModulesV1: ModuleV1Prefix, - MotdV1: "/api/terraform/motd", - StateV2: APIPrefixV2, - TfeV2: APIPrefixV2, - TfeV21: APIPrefixV2, - TfeV22: APIPrefixV2, - LoginV1: Discovery, + res := tfe.MotdResponse{Msg: tfe.MotdMessage()} + return c.JSON(200, res) } // Update GetWellKnownJson to use real OAuth endpoints and client ID @@ -67,26 +29,26 @@ func (h *TfeHandler) GetWellKnownJson(c echo.Context) error { c.Response().Header().Set("X-Terraform-Enterprise-App", "Terraform Enterprise") baseURL := getBaseURL(c) - + // Get the real client ID from environment (same as auth handler) clientID := os.Getenv("OPENTACO_AUTH_CLIENT_ID") if clientID == "" { clientID = "terraform" // fallback for compatibility } - + // Use the same OAuth endpoints as the main auth flow - discoveryPayload := DiscoveryDef{ - ModulesV1: ModuleV1Prefix, - MotdV1: "/api/terraform/motd", - StateV2: APIPrefixV2, - TfeV2: APIPrefixV2, - TfeV21: APIPrefixV2, - TfeV22: APIPrefixV2, - LoginV1: DiscoverySpec{ - Client: clientID, // Use real client ID + discoveryPayload := tfe.WellKnownSpec{ + Modules: ModuleV1Prefix, + MessageOfTheDay: "/tfe/api/v2/motd", + State: APIPrefixV2, + TfeApiV2: APIPrefixV2, + TfeApiV21: APIPrefixV2, + TfeApiV22: APIPrefixV2, + Login: tfe.LoginSpec{ + Client: clientID, // Use real client ID GrantTypes: []string{"authz_code"}, - Authz: baseURL + "/oauth/authorization", // Real OAuth endpoint - Token: baseURL + "/oauth/token", // Real OAuth endpoint + Authz: baseURL + "/oauth/authorization", // Real OAuth endpoint + Token: baseURL + "/oauth/token", // Real OAuth endpoint Ports: []int{10000, 10010}, }, } diff --git a/taco/internal/tfe/workspaces.go b/taco/internal/tfe/workspaces.go index 1c94be321..7207c7cfa 100644 --- a/taco/internal/tfe/workspaces.go +++ b/taco/internal/tfe/workspaces.go @@ -5,12 +5,12 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/diggerhq/digger/opentaco/internal/domain/tfe" "io" "net/http" "strings" "time" - "github.com/diggerhq/digger/opentaco/internal/domain" "github.com/diggerhq/digger/opentaco/internal/rbac" "github.com/diggerhq/digger/opentaco/internal/storage" "github.com/google/jsonapi" @@ -38,9 +38,9 @@ func parseStateVersionID(stateVersionID string) (stateID string, err error) { if !strings.HasPrefix(stateVersionID, "sv-") { return "", fmt.Errorf("invalid state version ID format: missing sv- prefix") } - + rest := stateVersionID[3:] - + // Find the timestamp part (should be all digits after last hyphen) // We need to be careful since base58 does not contain hyphens (unlike base64url) idx := -1 @@ -54,24 +54,24 @@ func parseStateVersionID(stateVersionID string) (stateID string, err error) { } } } - + if idx <= 0 || idx >= len(rest)-1 { return "", fmt.Errorf("invalid state version ID format: cannot find timestamp separator") } - + encodedStateID := rest[:idx] - + // Decode the base58-encoded state ID stateIDBytes, err := base58.Decode(encodedStateID) if err != nil { return "", fmt.Errorf("invalid state version ID encoding: %w", err) } - + stateID = string(stateIDBytes) if len(stateID) == 0 { return "", fmt.Errorf("decoded state ID is empty") } - + return stateID, nil } @@ -89,7 +89,7 @@ func convertWorkspaceToStateID(workspaceID string) string { if strings.TrimSpace(workspaceID) == "" { return "" } - + // If it's a TFE workspace ID (ws-something), extract just the workspace name if strings.HasPrefix(workspaceID, "ws-") { result := strings.TrimPrefix(workspaceID, "ws-") @@ -109,7 +109,7 @@ func extractWorkspaceIDFromParam(c echo.Context) string { // Fallback to workspace_name for routes that use that parameter workspaceName := c.Param("workspace_name") if workspaceName != "" { - return domain.NewTfeIDWithVal(domain.WorkspaceKind, workspaceName).String() + return tfe.NewTfeResourceIdentifier(tfe.WorkspaceType, workspaceName).String() } } return workspaceID @@ -137,7 +137,7 @@ func (h *TfeHandler) checkWorkspacePermission(c echo.Context, action string, wor // Scenario 3: RBAC is initialized → enforce permissions stateID := convertWorkspaceToStateID(workspaceID) - + // Extract user subject from JWT token in Authorization header authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { @@ -147,10 +147,10 @@ func (h *TfeHandler) checkWorkspacePermission(c echo.Context, action string, wor if !strings.HasPrefix(authHeader, "Bearer ") { return fmt.Errorf("invalid authorization header format") } - + // Extract and verify JWT token to get user principal token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")) - + // Get signer from auth handler to verify the token signer := h.authHandler.GetSigner() if signer == nil { @@ -158,7 +158,7 @@ func (h *TfeHandler) checkWorkspacePermission(c echo.Context, action string, wor principal := rbac.Principal{Subject: "unknown"} // Continue with permission check using unknown subject var rbacAction rbac.Action - + switch action { case "unit:read": rbacAction = rbac.ActionUnitRead @@ -169,19 +169,19 @@ func (h *TfeHandler) checkWorkspacePermission(c echo.Context, action string, wor default: return fmt.Errorf("unknown action: %s", action) } - + // Check permission using RBAC manager allowed, err := h.rbacManager.Can(c.Request().Context(), principal, rbacAction, stateID) if err != nil { return fmt.Errorf("failed to check permissions: %v", err) } - + if !allowed { return fmt.Errorf("insufficient permissions") } return nil } - + // TFE endpoints: verify opaque token only (for clear API boundaries) var principal rbac.Principal if h.apiTokens != nil { @@ -199,7 +199,7 @@ func (h *TfeHandler) checkWorkspacePermission(c echo.Context, action string, wor return fmt.Errorf("API token manager not available") } var rbacAction rbac.Action - + switch action { case "unit:read": rbacAction = rbac.ActionUnitRead @@ -224,86 +224,6 @@ func (h *TfeHandler) checkWorkspacePermission(c echo.Context, action string, wor return nil } -// Adapted from OTF (MPL License): https://github.com/leg100/otf -func ToTFE(from *domain.Workspace) (*domain.TFEWorkspace, error) { - perms := &domain.TFEWorkspacePermissions{ - CanLock: true, - CanUnlock: true, - CanForceUnlock: true, - CanQueueApply: true, - CanQueueDestroy: true, - CanQueueRun: true, - CanDestroy: true, - CanReadSettings: true, - CanUpdate: true, - CanUpdateVariable: true, - } - - to := &domain.TFEWorkspace{ - ID: from.ID, - Actions: &domain.TFEWorkspaceActions{ - IsDestroyable: true, - }, - AllowDestroyPlan: from.AllowDestroyPlan, - AgentPoolID: from.AgentPoolID, - AutoApply: from.AutoApply, - CanQueueDestroyPlan: from.CanQueueDestroyPlan, - CreatedAt: from.CreatedAt, - Description: from.Description, - Environment: from.Environment, - ExecutionMode: string(from.ExecutionMode), - GlobalRemoteState: from.GlobalRemoteState, - Locked: from.Locked(), - MigrationEnvironment: from.MigrationEnvironment, - Name: from.Name.Name, - // Operations is deprecated but clients and go-tfe tests still use it - Operations: from.ExecutionMode == "remote", - Permissions: perms, - QueueAllRuns: from.QueueAllRuns, - SpeculativeEnabled: from.SpeculativeEnabled, - SourceName: from.SourceName, - SourceURL: from.SourceURL, - StructuredRunOutputEnabled: from.StructuredRunOutputEnabled, - TerraformVersion: from.EngineVersion, - TriggerPrefixes: from.TriggerPrefixes, - TriggerPatterns: from.TriggerPatterns, - WorkingDirectory: from.WorkingDirectory, - TagNames: from.Tags, - UpdatedAt: from.UpdatedAt, - Organization: &domain.TFEOrganization{Name: from.Organization.Name}, - } - if len(from.TriggerPrefixes) > 0 || len(from.TriggerPatterns) > 0 { - to.FileTriggersEnabled = true - } - if from.LatestRun != nil { - to.CurrentRun = &domain.TFERun{ID: from.LatestRun.ID} - } - - // Add VCS repo to json:api struct if connected. NOTE: the terraform CLI - // uses the presence of VCS repo to determine whether to allow a terraform - // apply or not, displaying the following message if not: - // - // Apply not allowed for workspaces with a VCS connection - // - // A workspace that is connected to a VCS requires the VCS-driven workflow to ensure that the VCS remains the single source of truth. - // - // OTF permits the user to disable this behaviour by ommiting this info and - // fool the terraform CLI into thinking its not a workspace with a VCS - // connection. - if from.Connection != nil { - isTerraformCli := true // TODO: read from header - if !from.Connection.AllowCLIApply || !isTerraformCli { - to.VCSRepo = &domain.TFEVCSRepo{ - OAuthTokenID: from.Connection.VCSProviderID, - Branch: from.Connection.Branch, - Identifier: from.Connection.Repo, - TagsRegex: from.Connection.TagsRegex, - } - } - } - return to, nil -} - func (h *TfeHandler) GetWorkspace(c echo.Context) error { c.Response().Header().Set(echo.HeaderContentType, "application/vnd.api+json") c.Response().Header().Set("Tfp-Api-Version", "2.5") @@ -314,47 +234,50 @@ func (h *TfeHandler) GetWorkspace(c echo.Context) error { return c.JSON(400, map[string]string{"error": "workspace_name invalid"}) } - workspace := domain.Workspace{ - ID: domain.NewTfeIDWithVal(domain.WorkspaceKind, workspaceName).String(), - CreatedAt: time.Time{}, - UpdatedAt: time.Time{}, - AgentPoolID: domain.NewTfeIDWithVal(domain.AgentPoolKind, "HzEaJWMP5YTatZaS").String(), + workspace := &tfe.TFEWorkspace{ + ID: tfe.NewTfeResourceIdentifier(tfe.WorkspaceType, workspaceName).String(), + Actions: &tfe.TFEWorkspaceActions{IsDestroyable: true}, + AgentPoolID: tfe.NewTfeResourceIdentifier(tfe.AgentPoolType, "HzEaJWMP5YTatZaS").String(), AllowDestroyPlan: false, AutoApply: false, CanQueueDestroyPlan: false, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, Description: workspaceName, Environment: workspaceName, ExecutionMode: "local", + FileTriggersEnabled: false, GlobalRemoteState: false, + Locked: false, MigrationEnvironment: "", - Name: domain.NewName(workspaceName), + Name: workspaceName, + Operations: false, + Permissions: nil, QueueAllRuns: false, SpeculativeEnabled: false, - StructuredRunOutputEnabled: false, SourceName: "", SourceURL: "", - WorkingDirectory: "", - Organization: domain.NewName("opentaco"), - LatestRun: nil, - Tags: nil, - Lock: nil, - Engine: "", - EngineVersion: nil, - Connection: nil, - TriggerPatterns: nil, + StructuredRunOutputEnabled: false, + TerraformVersion: nil, TriggerPrefixes: nil, + TriggerPatterns: nil, + VCSRepo: nil, + WorkingDirectory: "", + ResourceCount: 0, + ApplyDurationAverage: 0, + PlanDurationAverage: 0, + PolicyCheckFailures: 0, + RunFailures: 0, + RunsCount: 0, + TagNames: nil, + CurrentRun: nil, + Organization: &tfe.TFEOrganization{ + Name: "opentaco", + }, + Outputs: nil, } - converted, err := ToTFE(&workspace) - if err != nil { - return err - } - - // Debug: Log the workspace data being sent - fmt.Printf("GetWorkspace: Sending workspace with ExecutionMode=%s, Operations=%t\n", - converted.ExecutionMode, converted.Operations) - - if err := jsonapi.MarshalPayload(c.Response().Writer, converted); err != nil { + if err := jsonapi.MarshalPayload(c.Response().Writer, workspace); err != nil { fmt.Printf("error marshaling workspace payload: %v", err) return err } @@ -371,7 +294,7 @@ func (h *TfeHandler) LockWorkspace(c echo.Context) error { if workspaceID == "" { return c.JSON(400, map[string]string{"error": "workspace_id required"}) } - + // Debug logging fmt.Printf("LockWorkspace: workspaceID=%s\n", workspaceID) @@ -382,7 +305,7 @@ func (h *TfeHandler) LockWorkspace(c echo.Context) error { "hint": "contact your administrator to grant unit:write permission", }) } - + if h.stateStore == nil { fmt.Printf("LockWorkspace: stateStore is nil!\n") return c.JSON(500, map[string]string{"error": "State store not initialized"}) @@ -644,9 +567,9 @@ func (h *TfeHandler) GetCurrentStateVersion(c echo.Context) error { "id": stateVersionID, "type": "state-versions", "attributes": map[string]interface{}{ - "created-at": stateMeta.Updated.UTC().Format(time.RFC3339), - "updated-at": stateMeta.Updated.UTC().Format(time.RFC3339), - "size": stateMeta.Size, + "created-at": stateMeta.Updated.UTC().Format(time.RFC3339), + "updated-at": stateMeta.Updated.UTC().Format(time.RFC3339), + "size": stateMeta.Size, "hosted-state-download-url": downloadURL, }, "relationships": map[string]interface{}{ @@ -747,49 +670,50 @@ func (h *TfeHandler) CreateStateVersion(c echo.Context) error { "error": "Failed to check state existence", }) } - + // Generate a state version ID (before upload) based on state ID and current time stateVersionID := generateStateVersionID(stateID, time.Now().Unix()) fmt.Printf("CreateStateVersion: Returning pending stateVersionID=%s (awaiting upload)\n", stateVersionID) - + // Build URLs baseURL := getBaseURL(c) uploadURL := fmt.Sprintf("%s/tfe/api/v2/state-versions/%s/upload", baseURL, stateVersionID) downloadURL := fmt.Sprintf("%s/tfe/api/v2/state-versions/%s/download", baseURL, stateVersionID) jsonUploadURL := fmt.Sprintf("%s/tfe/api/v2/state-versions/%s/json-upload", baseURL, stateVersionID) - + // Derive serial and lineage from existing state (if any) serial := 0 lineage := "" if stateBytes, dErr := h.stateStore.Download(c.Request().Context(), stateID); dErr == nil { var st map[string]interface{} if uErr := json.Unmarshal(stateBytes, &st); uErr == nil { - if v, ok := st["serial"].(float64); ok { serial = int(v) } - if v, ok := st["lineage"].(string); ok { lineage = v } + if v, ok := st["serial"].(float64); ok { + serial = int(v) + } + if v, ok := st["lineage"].(string); ok { + lineage = v + } } } - - - fmt.Printf("CreateStateVersion: baseURL='%s'\n", baseURL) fmt.Printf("CreateStateVersion: uploadURL='%s'\n", uploadURL) - + // Build the response response := map[string]interface{}{ "data": map[string]interface{}{ "id": stateVersionID, "type": "state-versions", "attributes": map[string]interface{}{ - "created-at": time.Now().UTC().Format(time.RFC3339), - "updated-at": time.Now().UTC().Format(time.RFC3339), - "size": 0, - "upload-url": uploadURL, - "hosted-state-upload-url": uploadURL, - "hosted-state-download-url": downloadURL, - "hosted-json-state-upload-url": jsonUploadURL, - "serial": serial, - "lineage": lineage, + "created-at": time.Now().UTC().Format(time.RFC3339), + "updated-at": time.Now().UTC().Format(time.RFC3339), + "size": 0, + "upload-url": uploadURL, + "hosted-state-upload-url": uploadURL, + "hosted-state-download-url": downloadURL, + "hosted-json-state-upload-url": jsonUploadURL, + "serial": serial, + "lineage": lineage, }, "relationships": map[string]interface{}{ "workspace": map[string]interface{}{ @@ -801,7 +725,7 @@ func (h *TfeHandler) CreateStateVersion(c echo.Context) error { }, }, } - + // Convert to actual JSON to see what gets sent to Terraform jsonBytes, err := json.Marshal(response) if err != nil { @@ -809,7 +733,7 @@ func (h *TfeHandler) CreateStateVersion(c echo.Context) error { return c.JSON(500, map[string]string{"error": "Failed to create response"}) } fmt.Printf("CreateStateVersion: Actual JSON being sent: %s\n", string(jsonBytes)) - + return c.JSON(201, response) } @@ -833,13 +757,13 @@ func (h *TfeHandler) CreateStateVersionDirect(c echo.Context, workspaceID, state if lockErr != nil && lockErr != storage.ErrNotFound { return c.JSON(500, map[string]string{"error": "Failed to get lock status"}) } - + // Extract lock ID if state is locked lockID := "" if currentLock != nil { lockID = currentLock.ID } - + // Upload the state with proper lock ID err = h.stateStore.Upload(c.Request().Context(), stateID, body, lockID) if err != nil { @@ -913,16 +837,16 @@ func (h *TfeHandler) DownloadStateVersion(c echo.Context) error { // UploadStateVersion handles PUT /tfe/api/v2/state-versions/:id/upload func (h *TfeHandler) UploadStateVersion(c echo.Context) error { fmt.Printf("UploadStateVersion: START - Method=%s, URI=%s\n", c.Request().Method, c.Request().RequestURI) - + // Debug: Check if Authorization header is present authHeader := c.Request().Header.Get("Authorization") fmt.Printf("UploadStateVersion: Authorization header present: %t\n", authHeader != "") if authHeader != "" { // Don't log the full token for security, just whether it looks like a Bearer token - fmt.Printf("UploadStateVersion: Authorization header format: %s\n", + fmt.Printf("UploadStateVersion: Authorization header format: %s\n", strings.SplitN(authHeader, " ", 2)[0]) } - + stateVersionID := c.Param("id") fmt.Printf("UploadStateVersion: stateVersionID=%s\n", stateVersionID) if stateVersionID == "" { @@ -936,7 +860,7 @@ func (h *TfeHandler) UploadStateVersion(c echo.Context) error { return c.JSON(400, map[string]string{"error": "Invalid state version ID format"}) } workspaceID := stateID // For TFE, workspace ID matches state ID - + // Check RBAC permission for uploading state (if auth available) // Note: Upload endpoints are exempt from auth middleware since Terraform doesn't send headers // Security relies on: valid upload URL + lock ownership + this RBAC check when possible @@ -983,13 +907,13 @@ func (h *TfeHandler) UploadStateVersion(c echo.Context) error { if lockErr != nil && lockErr != storage.ErrNotFound { return c.JSON(500, map[string]string{"error": "Failed to get lock status"}) } - + // Extract lock ID if state is locked lockID := "" if currentLock != nil { lockID = currentLock.ID } - + // Upload the state with proper lock ID fmt.Printf("UploadStateVersion: Uploading to storage with lockID=%s\n", lockID) err = h.stateStore.Upload(c.Request().Context(), stateID, stateData, lockID) @@ -1012,118 +936,118 @@ func (h *TfeHandler) UploadStateVersion(c echo.Context) error { return c.NoContent(204) } +func (h *TfeHandler) UploadJSONStateOutputs(c echo.Context) error { + id := c.Param("id") + fmt.Printf("UploadJSONStateOutputs: stateVersionID=%s\n", id) + // Debug: Check if Authorization header is present + authHeader := c.Request().Header.Get("Authorization") + fmt.Printf("UploadJSONStateOutputs: Authorization header present: %t\n", authHeader != "") + if authHeader != "" { + fmt.Printf("UploadJSONStateOutputs: Authorization header format: %s\n", + strings.SplitN(authHeader, " ", 2)[0]) + } + // Parse state version ID to get workspace ID for RBAC check + stateID, err := parseStateVersionID(id) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid state version ID format"}) + } + workspaceID := stateID // For TFE, workspace ID matches state ID -func (h *TfeHandler) UploadJSONStateOutputs(c echo.Context) error { - id := c.Param("id") - fmt.Printf("UploadJSONStateOutputs: stateVersionID=%s\n", id) - - // Debug: Check if Authorization header is present - authHeader := c.Request().Header.Get("Authorization") - fmt.Printf("UploadJSONStateOutputs: Authorization header present: %t\n", authHeader != "") - if authHeader != "" { - fmt.Printf("UploadJSONStateOutputs: Authorization header format: %s\n", - strings.SplitN(authHeader, " ", 2)[0]) - } - - // Parse state version ID to get workspace ID for RBAC check - stateID, err := parseStateVersionID(id) - if err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid state version ID format"}) - } - workspaceID := stateID // For TFE, workspace ID matches state ID - - // Check RBAC permission for uploading state outputs (if auth available) - // Note: Upload endpoints are exempt from auth middleware since Terraform doesn't send headers - // Security relies on: valid upload URL + lock ownership + this RBAC check when possible - if err := h.checkWorkspacePermission(c, "unit:write", workspaceID); err != nil { - // Only enforce RBAC if we have a real auth error, not just missing headers - if !strings.Contains(err.Error(), "no authorization header") { - fmt.Printf("UploadJSONStateOutputs: RBAC permission denied: %v\n", err) - return c.JSON(http.StatusForbidden, map[string]string{ - "error": "insufficient permissions to upload state outputs", - "hint": "contact your administrator to grant state:write permission", - }) - } - // If no auth header, allow but log for security monitoring - fmt.Printf("UploadJSONStateOutputs: No auth header - allowing upload based on lock validation\n") - } - - body, err := io.ReadAll(c.Request().Body) - if err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Failed to read outputs"}) - } - if len(body) > 0 { - fmt.Printf("UploadJSONStateOutputs: %d bytes (preview: %s)\n", len(body), string(body[:min(200, len(body))])) - } - return c.NoContent(http.StatusNoContent) // -} + // Check RBAC permission for uploading state outputs (if auth available) + // Note: Upload endpoints are exempt from auth middleware since Terraform doesn't send headers + // Security relies on: valid upload URL + lock ownership + this RBAC check when possible + if err := h.checkWorkspacePermission(c, "unit:write", workspaceID); err != nil { + // Only enforce RBAC if we have a real auth error, not just missing headers + if !strings.Contains(err.Error(), "no authorization header") { + fmt.Printf("UploadJSONStateOutputs: RBAC permission denied: %v\n", err) + return c.JSON(http.StatusForbidden, map[string]string{ + "error": "insufficient permissions to upload state outputs", + "hint": "contact your administrator to grant state:write permission", + }) + } + // If no auth header, allow but log for security monitoring + fmt.Printf("UploadJSONStateOutputs: No auth header - allowing upload based on lock validation\n") + } + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Failed to read outputs"}) + } + if len(body) > 0 { + fmt.Printf("UploadJSONStateOutputs: %d bytes (preview: %s)\n", len(body), string(body[:min(200, len(body))])) + } + return c.NoContent(http.StatusNoContent) // +} func (h *TfeHandler) ShowStateVersion(c echo.Context) error { - c.Response().Header().Set(echo.HeaderContentType, "application/vnd.api+json") - id := c.Param("id") - if id == "" || !strings.HasPrefix(id, "sv-") { - return c.JSON(http.StatusNotFound, map[string]interface{}{ - "errors": []map[string]string{{"status":"404","title":"not_found"}}, - }) - } - - // Parse state version ID to extract state ID - stateID, err := parseStateVersionID(id) - if err != nil { - return c.JSON(http.StatusNotFound, map[string]interface{}{ - "errors": []map[string]string{{"status":"404","title":"invalid_id"}}, - }) - } - - // Load metadata (and optionally content) - meta, err := h.stateStore.Get(c.Request().Context(), stateID) - if err != nil { - if err == storage.ErrNotFound { - return c.JSON(http.StatusNotFound, map[string]interface{}{ - "errors": []map[string]string{{"status":"404","title":"state_not_found"}}, - }) - } - return c.JSON(http.StatusInternalServerError, map[string]string{"error":"state_meta_error"}) - } - - // Optional: extract serial/lineage + md5 - var serial int - var lineage, md5b64 string - if bytes, dErr := h.stateStore.Download(c.Request().Context(), stateID); dErr == nil && len(bytes) > 0 { - var st map[string]interface{} - if json.Unmarshal(bytes, &st) == nil { - if v, ok := st["serial"].(float64); ok { serial = int(v) } - if v, ok := st["lineage"].(string); ok { lineage = v } - } - sum := md5.Sum(bytes) - md5b64 = base64.StdEncoding.EncodeToString(sum[:]) - } - - baseURL := getBaseURL(c) - downloadURL := fmt.Sprintf("%s/tfe/api/v2/state-versions/%s/download", baseURL, id) - - resp := map[string]interface{}{ - "data": map[string]interface{}{ - "id": id, - "type": "state-versions", - "attributes": map[string]interface{}{ - "created-at": meta.Updated.UTC().Format(time.RFC3339), - "updated-at": meta.Updated.UTC().Format(time.RFC3339), - "size": meta.Size, - "serial": serial, - "lineage": lineage, - "md5": md5b64, // optional - "hosted-state-download-url": downloadURL, - }, - "relationships": map[string]interface{}{ - "workspace": map[string]interface{}{ - "data": map[string]interface{}{"id": stateID, "type": "workspaces"}, - }, - }, - }, - } - return c.JSON(http.StatusOK, resp) -} \ No newline at end of file + c.Response().Header().Set(echo.HeaderContentType, "application/vnd.api+json") + id := c.Param("id") + if id == "" || !strings.HasPrefix(id, "sv-") { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "errors": []map[string]string{{"status": "404", "title": "not_found"}}, + }) + } + + // Parse state version ID to extract state ID + stateID, err := parseStateVersionID(id) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "errors": []map[string]string{{"status": "404", "title": "invalid_id"}}, + }) + } + + // Load metadata (and optionally content) + meta, err := h.stateStore.Get(c.Request().Context(), stateID) + if err != nil { + if err == storage.ErrNotFound { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "errors": []map[string]string{{"status": "404", "title": "state_not_found"}}, + }) + } + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "state_meta_error"}) + } + + // Optional: extract serial/lineage + md5 + var serial int + var lineage, md5b64 string + if bytes, dErr := h.stateStore.Download(c.Request().Context(), stateID); dErr == nil && len(bytes) > 0 { + var st map[string]interface{} + if json.Unmarshal(bytes, &st) == nil { + if v, ok := st["serial"].(float64); ok { + serial = int(v) + } + if v, ok := st["lineage"].(string); ok { + lineage = v + } + } + sum := md5.Sum(bytes) + md5b64 = base64.StdEncoding.EncodeToString(sum[:]) + } + + baseURL := getBaseURL(c) + downloadURL := fmt.Sprintf("%s/tfe/api/v2/state-versions/%s/download", baseURL, id) + + resp := map[string]interface{}{ + "data": map[string]interface{}{ + "id": id, + "type": "state-versions", + "attributes": map[string]interface{}{ + "created-at": meta.Updated.UTC().Format(time.RFC3339), + "updated-at": meta.Updated.UTC().Format(time.RFC3339), + "size": meta.Size, + "serial": serial, + "lineage": lineage, + "md5": md5b64, // optional + "hosted-state-download-url": downloadURL, + }, + "relationships": map[string]interface{}{ + "workspace": map[string]interface{}{ + "data": map[string]interface{}{"id": stateID, "type": "workspaces"}, + }, + }, + }, + } + return c.JSON(http.StatusOK, resp) +}