From 5f63cb9c08946ab90ca5b3c4fb7dde7ae1a27628 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:02:45 -0700 Subject: [PATCH 01/13] chore: pin the sdk at the credential-broker branch head --- .gitignore | 1 + go.mod | 13 +++++++++++-- go.sum | 57 ++++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 9bbcd5fb..d8071d08 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ CLAUDE.md .claude/ .handoffs/ .worktrees/ +.superpowers/ diff --git a/go.mod b/go.mod index 0ce1d626..d7991c73 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,9 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/google/uuid v1.6.0 github.com/platform-engineering-labs/formae/pkg/model v0.1.26 - github.com/platform-engineering-labs/formae/pkg/plugin v0.4.1 + // SDK pinned at the credential-broker branch head. Re-pin to a real tag + // before merge (release-path work). + github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821030446-6aa3db2765ec github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630 github.com/stretchr/testify v1.11.1 ) @@ -73,6 +75,10 @@ require ( github.com/miekg/dns v1.1.72 // indirect github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df // indirect github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 // indirect + // pkg/plugin's own require for this is a zero-value placeholder (its + // replace directive is repo-local and ignored by consumers), so it must + // be pinned explicitly here. Re-pin to a real tag before merge (release-path work). + github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355 // indirect github.com/platform-engineering-labs/orbital v0.1.36 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect @@ -117,6 +123,9 @@ require ( resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027 // indirect ) -replace ergo.services/ergo => github.com/JeroenSoeters/ergo v1.999.320-pel.2 +// Matches the ergo fork revision required by the pinned pkg/plugin and +// pkg/credential (both need -pel.6). Re-pin to a real tag before merge +// (release-path work). +replace ergo.services/ergo => github.com/JeroenSoeters/ergo v1.999.320-pel.6 replace ergo.services/actor/statemachine => github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4 diff --git a/go.sum b/go.sum index a1494074..d7585392 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,24 @@ +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4 h1:JdoPbqQSv9iDCfkBFH5wDZCNpYHgJJVsrD7Xg0Jp57o= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4/go.mod h1:XbiEudzYCbg5YvbIMqylWXH2iWnUeKagDTfPR7uOaqg= -github.com/JeroenSoeters/ergo v1.999.320-pel.2 h1:UL0ROEToN9mmkXobs4qBHKiWodHJNkbZdx9bXRd5b8c= -github.com/JeroenSoeters/ergo v1.999.320-pel.2/go.mod h1:bLQ6PoO6Mz/8gVuzvPv3xfMfo1P9w6rZV1WnMXMeMdg= +github.com/JeroenSoeters/ergo v1.999.320-pel.6 h1:aq/v/tYTn+/QJZuMV6GDDnln2EUI+Wy3cVFhs7TB1lU= +github.com/JeroenSoeters/ergo v1.999.320-pel.6/go.mod h1:bLQ6PoO6Mz/8gVuzvPv3xfMfo1P9w6rZV1WnMXMeMdg= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8 h1:6+qQvLkethJZQLb6r+Jxh0qxa7TfeFPBHlCzF7Ml8CM= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apple/pkl-go v0.13.2 h1:UCug0neH9Ufw0Loujosv5/UUXEXzY76HXbjqYqlPSGI= github.com/apple/pkl-go v0.13.2/go.mod h1:Ko3AgXOKd/vVYtsRZgoCZhymymz9RxqCIcfdZhOX85I= github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= @@ -28,6 +39,7 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 h1:5CrzwxDqf4w3x1Vs3 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36/go.mod h1:A3gHdKZIvG/QXERzZwcxNS3RNDFcRCuhhTFBYp+V/nw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 h1:A4N2f4YPcST0v+dWtX+xrpPPCL9VTBhoIFFUWYqbacE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36/go.mod h1:B/Qr859uxWUEfZeGotK5KAEoof4Q9YWgNtPSwV6jcyk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= github.com/aws/aws-sdk-go-v2/service/acm v1.39.4 h1:2P7p/kNLozilMJfF5SNfKCAslLZFtLmr7RjDHVni024= @@ -92,16 +104,23 @@ github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -110,10 +129,13 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= github.com/goforj/godump v1.9.1 h1:9OGpb978Ytz3B59d5Yi2PzRYYLid6UkmhYDIDNiF15Y= github.com/goforj/godump v1.9.1/go.mod h1:JsuL6AEZfKIU+iR5ewL6iQ2fIuhvLtPmJDH47M9Ptrc= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -124,6 +146,9 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -140,16 +165,18 @@ github.com/masterminds/semver v1.5.0 h1:hTxJTTY7tjvnWMrl08O6u3G6BLlKVwxSz01lVac9 github.com/masterminds/semver v1.5.0/go.mod h1:s7KNT9fnd7edGzwwP7RBX4H0v/CYd5qdOLfkL1V75yg= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df h1:1xWk/De6cs3h2rv2LZA/jcLFTmEGD9EluHsKaYq+Gm0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df/go.mod h1:sTJHuiEPB0WNdPYPOP9M6FQU4YTHemXgDLIQqkbBdDI= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 h1:ZMTgKwSomy2cVcl/+NivSqopbWeHbmYeQ+BxoYq8bVY= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1/go.mod h1:0ncHFCsGA6b0w1kBm6m+QwJ823qAY2vL47GvoR0BTyU= +github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355 h1:em+eKL2p7m2L/LNbPThCzPefjtGcEPfnDM2YcQpGVIs= +github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355/go.mod h1:VS9hO24cYUcGYao9nfPb41HBfE2hF4WMrIS2fZwRwzM= github.com/platform-engineering-labs/formae/pkg/model v0.1.26 h1:80p843bmz9sLTtUFveMGWYeGcpRjvceyVxUgPvlApqs= github.com/platform-engineering-labs/formae/pkg/model v0.1.26/go.mod h1:1dmsFwoaJZkHevBsAIZr068CWzG7de1VNz7PFWvM3Z8= -github.com/platform-engineering-labs/formae/pkg/plugin v0.4.1 h1:dg8TBQVJR8DVF28bvmzAG4Ms1Id0yIr6Kd8c3UTO3iw= -github.com/platform-engineering-labs/formae/pkg/plugin v0.4.1/go.mod h1:ZFXMfeZljHVDWQTSeJ4dj2EG3iTjdrxQjm7DpvOYUXk= -github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260810030146-5a3389594c8e h1:EvXbhuvwOq3CIyx+pin9uyenol6IvBJFhkO/rdIAKm8= -github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260810030146-5a3389594c8e/go.mod h1:gq591ZeRZ4jAHpdSvCyrOvi8qyALxcXQQhd0B6GRMAo= +github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821030446-6aa3db2765ec h1:yrTUKa3bVNd3pr/tCcBx/onUdP39i7zFopuuzcVQC6M= +github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821030446-6aa3db2765ec/go.mod h1:2XJE4ZFchjdr+IDJz39PC9D41ziYe5lp1kAxpBWUD7w= github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630 h1:ZyGSai5xPIi+vxAS04JlVr04Fecw3K9Zj2bsR4OBkdE= github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630/go.mod h1:gq591ZeRZ4jAHpdSvCyrOvi8qyALxcXQQhd0B6GRMAo= github.com/platform-engineering-labs/orbital v0.1.36 h1:nPMLxDbwDrjlhJoMQXAE86HtVhQHC2A6BJlWYmNM75c= @@ -158,12 +185,16 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -190,14 +221,18 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246 h1:OB/t6xQE5QbY6m4bfg5f3kLCse6hL+X46HeQIG3F1lA= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246/go.mod h1:W9MwwilRxUgtLgXlceriUCNwM5j1lcMNAAq3Ya47C8Y= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/host v0.65.0 h1:cR4LpCn/2xDNdW3saBLrGJW7vWmrYlHYIhfuklhrlUc= go.opentelemetry.io/contrib/instrumentation/host v0.65.0/go.mod h1:laAqufqDgLYaaewUBpolv8GePmhIVqIeHyudbmi9KYk= go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ= @@ -218,10 +253,15 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -229,12 +269,16 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= +gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= @@ -252,3 +296,4 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027 h1:VSL2fw4r13Ve7yVD4RUlCfSlGmZF4AtJBv0fA4PhPvo= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 753742bb80af6a8329345bbd650b4023c7b33b0b Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:06:35 -0700 Subject: [PATCH 02/13] feat(schema): polymorphic auth on the target config --- schema/pkl/aws.pkl | 35 ++++++ schema_auth_test.go | 109 ++++++++++++++++++ .../aws-config-auth-both-set-rejected.pkl | 31 +++++ testdata/aws-config-auth-defaultchain.pkl | 30 +++++ testdata/aws-config-auth-flat.pkl | 28 +++++ testdata/aws-config-auth-oidc.pkl | 30 +++++ 6 files changed, 263 insertions(+) create mode 100644 schema_auth_test.go create mode 100644 testdata/aws-config-auth-both-set-rejected.pkl create mode 100644 testdata/aws-config-auth-defaultchain.pkl create mode 100644 testdata/aws-config-auth-flat.pkl create mode 100644 testdata/aws-config-auth-oidc.pkl diff --git a/schema/pkl/aws.pkl b/schema/pkl/aws.pkl index 8376dc9d..07095848 100644 --- a/schema/pkl/aws.pkl +++ b/schema/pkl/aws.pkl @@ -85,12 +85,47 @@ typealias AvailabilityZone = String((str) -> ( open class Config { hidden fixed type: String = "AWS" + + /// Deprecated: legacy spelling of the default credential chain. + @Deprecated { message = "set auth = new DefaultChainAuth { profile = ... } instead" } hidden profile: String? + hidden region: Region + /// Authentication strategy. Omitting it means the default credential + /// chain, honouring the deprecated `profile` if set. Exactly one spelling + /// may be used: setting both `auth` and `profile` is rejected at eval. + @formae.ConfigFieldHint { createOnly = false } + hidden auth: Auth?(this == null || profile == null) + fixed Type: String = type fixed Profile: String? = profile fixed Region: Region = region + fixed Auth: Auth? = auth +} + +abstract class Auth { + hidden fixed type: String + fixed Type: String = type +} + +/// The AWS default credential provider chain (env vars, shared config, +/// IMDS/IRSA), optionally pinned to a shared-config profile. +class DefaultChainAuth extends Auth { + fixed type = "DefaultChain" + hidden profile: String? + fixed Profile: String? = profile +} + +/// Federated identity: an OIDC identity token from the paired +/// oidc-credential broker, exchanged for credentials by assuming this role. +class OidcAuth extends Auth { + fixed type = "Oidc" + /// The role in the customer account whose trust policy pins the + /// installation's identity. Everything else (issuer, key, token endpoint) + /// belongs to the broker, not the target. + hidden roleArn: String + fixed RoleArn: String = roleArn } class FieldHint extends formae.FieldHint {} diff --git a/schema_auth_test.go b/schema_auth_test.go new file mode 100644 index 00000000..4e854347 --- /dev/null +++ b/schema_auth_test.go @@ -0,0 +1,109 @@ +// © 2025 Platform Engineering Labs Inc. +// +// SPDX-License-Identifier: FSL-1.1-ALv2 + +//go:build unit + +package main + +// These tests exercise the polymorphic Auth schema on aws#Config through +// the real formae eval path: a forma file is rendered by a live formae +// binary against the plugin as installed by `make install`, and the +// resulting target Config JSON is asserted against its expected shape. +// +// They only run when FORMAE_BINARY points at a formae binary (they are +// skipped otherwise, e.g. in CI, which has no formae binary available). +// Before running locally: +// +// make install +// FORMAE_BINARY=/path/to/formae go test -tags=unit -run TestAuthSchema -v . + +import ( + "encoding/json" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type evalResult struct { + Targets []struct { + Config json.RawMessage `json:"Config"` + } `json:"Targets"` +} + +// runEval evaluates a testdata fixture with the formae binary named by +// FORMAE_BINARY and returns the target Config JSON of the first target. +// It fails the (sub)test if the binary is missing from FORMAE_BINARY, and +// returns the raw stderr and a non-nil error when evaluation itself fails +// so callers can assert on rejection. +func runEval(t *testing.T, binary, fixture string) (json.RawMessage, []byte, error) { + t.Helper() + + cmd := exec.Command(binary, "eval", //nolint:gosec // test-only, binary path comes from a trusted env var + "--output-consumer", "machine", + "--schema-location", "local", + fixture, + ) + cmd.Env = append(os.Environ(), "FORMAE_TEST_RUN_ID=unit") + + stdout, err := cmd.Output() + if err != nil { + var stderr []byte + if exitErr, ok := err.(*exec.ExitError); ok { + stderr = exitErr.Stderr + } + return nil, stderr, err + } + + var result evalResult + require.NoError(t, json.Unmarshal(stdout, &result), "eval output was not valid JSON: %s", stdout) + require.Len(t, result.Targets, 1, "expected exactly one target in eval output") + + return result.Targets[0].Config, nil, nil +} + +func TestAuthSchema(t *testing.T) { + binary := os.Getenv("FORMAE_BINARY") + if binary == "" { + t.Skip("FORMAE_BINARY not set; skipping formae-eval-backed schema tests") + } + + t.Run("flat profile with no auth block renders Profile and omits Auth", func(t *testing.T) { + config, _, err := runEval(t, binary, "testdata/aws-config-auth-flat.pkl") + require.NoError(t, err) + + assert.JSONEq(t, `{"Type":"AWS","Profile":"legacy-profile","Region":"us-east-1"}`, string(config)) + }) + + t.Run("auth = DefaultChainAuth renders a nested Auth object", func(t *testing.T) { + config, _, err := runEval(t, binary, "testdata/aws-config-auth-defaultchain.pkl") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "Type":"AWS", + "Region":"us-east-1", + "Auth":{"Type":"DefaultChain","Profile":"chain-profile"} + }`, string(config)) + }) + + t.Run("auth = OidcAuth renders a nested Auth object", func(t *testing.T) { + config, _, err := runEval(t, binary, "testdata/aws-config-auth-oidc.pkl") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "Type":"AWS", + "Region":"us-east-1", + "Auth":{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"} + }`, string(config)) + }) + + t.Run("setting both profile and auth is rejected at eval", func(t *testing.T) { + _, stderr, err := runEval(t, binary, "testdata/aws-config-auth-both-set-rejected.pkl") + require.Error(t, err) + assert.Contains(t, string(stderr), "Type constraint") + assert.Contains(t, string(stderr), "this == null || profile == null") + }) +} diff --git a/testdata/aws-config-auth-both-set-rejected.pkl b/testdata/aws-config-auth-both-set-rejected.pkl new file mode 100644 index 00000000..5b61ced6 --- /dev/null +++ b/testdata/aws-config-auth-both-set-rejected.pkl @@ -0,0 +1,31 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" +import "@formae/formae.pkl" + +import "@aws/aws.pkl" + +local testRunID = read("env:FORMAE_TEST_RUN_ID") +local stackName = "plugin-sdk-test-aws-config-auth-both-set-\(testRunID)" + +forma { + new formae.Stack { + label = stackName + description = "Plugin SDK fixture: both profile and auth set, expected to be rejected at eval" + } + + new formae.Target { + label = "aws-target" + config = new aws.Config { + region = "us-east-1" + profile = "legacy-profile" + auth = new aws.DefaultChainAuth { + profile = "chain-profile" + } + } + } +} diff --git a/testdata/aws-config-auth-defaultchain.pkl b/testdata/aws-config-auth-defaultchain.pkl new file mode 100644 index 00000000..30c3fb68 --- /dev/null +++ b/testdata/aws-config-auth-defaultchain.pkl @@ -0,0 +1,30 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" +import "@formae/formae.pkl" + +import "@aws/aws.pkl" + +local testRunID = read("env:FORMAE_TEST_RUN_ID") +local stackName = "plugin-sdk-test-aws-config-auth-defaultchain-\(testRunID)" + +forma { + new formae.Stack { + label = stackName + description = "Plugin SDK fixture: auth = DefaultChainAuth with a profile" + } + + new formae.Target { + label = "aws-target" + config = new aws.Config { + region = "us-east-1" + auth = new aws.DefaultChainAuth { + profile = "chain-profile" + } + } + } +} diff --git a/testdata/aws-config-auth-flat.pkl b/testdata/aws-config-auth-flat.pkl new file mode 100644 index 00000000..4af2f504 --- /dev/null +++ b/testdata/aws-config-auth-flat.pkl @@ -0,0 +1,28 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" +import "@formae/formae.pkl" + +import "@aws/aws.pkl" + +local testRunID = read("env:FORMAE_TEST_RUN_ID") +local stackName = "plugin-sdk-test-aws-config-auth-flat-\(testRunID)" + +forma { + new formae.Stack { + label = stackName + description = "Plugin SDK fixture: legacy flat profile, no auth block" + } + + new formae.Target { + label = "aws-target" + config = new aws.Config { + region = "us-east-1" + profile = "legacy-profile" + } + } +} diff --git a/testdata/aws-config-auth-oidc.pkl b/testdata/aws-config-auth-oidc.pkl new file mode 100644 index 00000000..1aa4557d --- /dev/null +++ b/testdata/aws-config-auth-oidc.pkl @@ -0,0 +1,30 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" +import "@formae/formae.pkl" + +import "@aws/aws.pkl" + +local testRunID = read("env:FORMAE_TEST_RUN_ID") +local stackName = "plugin-sdk-test-aws-config-auth-oidc-\(testRunID)" + +forma { + new formae.Stack { + label = stackName + description = "Plugin SDK fixture: auth = OidcAuth" + } + + new formae.Target { + label = "aws-target" + config = new aws.Config { + region = "us-east-1" + auth = new aws.OidcAuth { + roleArn = "arn:aws:iam::123456789012:role/formae-agent" + } + } + } +} From abbea28d10256b8f56de8868589d021878ccf7f5 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:11:53 -0700 Subject: [PATCH 03/13] chore: tidy go.sum after the sdk re-pin --- go.sum | 45 --------------------------------------------- 1 file changed, 45 deletions(-) diff --git a/go.sum b/go.sum index d7585392..1c01bebe 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,5 @@ -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= -codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= -codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= -git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4 h1:JdoPbqQSv9iDCfkBFH5wDZCNpYHgJJVsrD7Xg0Jp57o= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4/go.mod h1:XbiEudzYCbg5YvbIMqylWXH2iWnUeKagDTfPR7uOaqg= github.com/JeroenSoeters/ergo v1.999.320-pel.6 h1:aq/v/tYTn+/QJZuMV6GDDnln2EUI+Wy3cVFhs7TB1lU= @@ -15,10 +8,6 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8 h1:6+qQvLkethJZQLb6r+Jxh0qxa7TfeFPBHlCzF7Ml8CM= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apple/pkl-go v0.13.2 h1:UCug0neH9Ufw0Loujosv5/UUXEXzY76HXbjqYqlPSGI= github.com/apple/pkl-go v0.13.2/go.mod h1:Ko3AgXOKd/vVYtsRZgoCZhymymz9RxqCIcfdZhOX85I= github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= @@ -39,7 +28,6 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 h1:5CrzwxDqf4w3x1Vs3 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36/go.mod h1:A3gHdKZIvG/QXERzZwcxNS3RNDFcRCuhhTFBYp+V/nw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 h1:A4N2f4YPcST0v+dWtX+xrpPPCL9VTBhoIFFUWYqbacE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36/go.mod h1:B/Qr859uxWUEfZeGotK5KAEoof4Q9YWgNtPSwV6jcyk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= github.com/aws/aws-sdk-go-v2/service/acm v1.39.4 h1:2P7p/kNLozilMJfF5SNfKCAslLZFtLmr7RjDHVni024= @@ -104,23 +92,16 @@ github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -129,13 +110,10 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= github.com/goforj/godump v1.9.1 h1:9OGpb978Ytz3B59d5Yi2PzRYYLid6UkmhYDIDNiF15Y= github.com/goforj/godump v1.9.1/go.mod h1:JsuL6AEZfKIU+iR5ewL6iQ2fIuhvLtPmJDH47M9Ptrc= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -146,9 +124,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= -github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -165,10 +140,8 @@ github.com/masterminds/semver v1.5.0 h1:hTxJTTY7tjvnWMrl08O6u3G6BLlKVwxSz01lVac9 github.com/masterminds/semver v1.5.0/go.mod h1:s7KNT9fnd7edGzwwP7RBX4H0v/CYd5qdOLfkL1V75yg= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df h1:1xWk/De6cs3h2rv2LZA/jcLFTmEGD9EluHsKaYq+Gm0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df/go.mod h1:sTJHuiEPB0WNdPYPOP9M6FQU4YTHemXgDLIQqkbBdDI= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 h1:ZMTgKwSomy2cVcl/+NivSqopbWeHbmYeQ+BxoYq8bVY= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1/go.mod h1:0ncHFCsGA6b0w1kBm6m+QwJ823qAY2vL47GvoR0BTyU= github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355 h1:em+eKL2p7m2L/LNbPThCzPefjtGcEPfnDM2YcQpGVIs= @@ -185,16 +158,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -221,18 +190,14 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246 h1:OB/t6xQE5QbY6m4bfg5f3kLCse6hL+X46HeQIG3F1lA= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246/go.mod h1:W9MwwilRxUgtLgXlceriUCNwM5j1lcMNAAq3Ya47C8Y= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/host v0.65.0 h1:cR4LpCn/2xDNdW3saBLrGJW7vWmrYlHYIhfuklhrlUc= go.opentelemetry.io/contrib/instrumentation/host v0.65.0/go.mod h1:laAqufqDgLYaaewUBpolv8GePmhIVqIeHyudbmi9KYk= go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ= @@ -253,15 +218,10 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -269,16 +229,12 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= -gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= @@ -296,4 +252,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027 h1:VSL2fw4r13Ve7yVD4RUlCfSlGmZF4AtJBv0fA4PhPvo= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 3a06a3feb5066d36052734a7f2e9b2e272cb88b7 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:19:04 -0700 Subject: [PATCH 04/13] feat(config): dispatch on the polymorphic auth block Config now carries the nested Auth JSON block alongside the flat Region/Profile fields the Pkl schema renders it beside. effectiveAuth resolves the two into one discriminated block: an explicit Auth wins, an absent/null/whitespace-only Auth synthesises a DefaultChain from the flat Profile, and setting both is rejected in Go too, mirroring the Pkl-level constraint for callers that bypass the schema. ToAwsConfig keeps its signature and dispatches on DefaultChain | Oidc; an unknown discriminator errors. The Oidc arm requires a non-nil OidcDeps.Source and fails closed rather than ever falling back to ambient credentials; the actual STS/web-identity credential composition is a follow-up and currently returns a clear not-yet-implemented error. OidcDeps is owned per plugin instance (threaded via WithOidcDeps), not process-global, so its warn-once deprecation notice and future credentials cache stay instance-scoped and test order stays independent. Config values with no OidcDeps at all still see the flat-profile deprecation warning once per process via a package-level fallback. --- go.mod | 2 +- pkg/config/config.go | 214 ++++++++++++++++++++++++++- pkg/config/config_test.go | 304 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 512 insertions(+), 8 deletions(-) create mode 100644 pkg/config/config_test.go diff --git a/go.mod b/go.mod index d7991c73..3aad9ac1 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/apple/pkl-go v0.13.2 // indirect github.com/asdine/storm v2.1.2+incompatible // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.15 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.10 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 // indirect diff --git a/pkg/config/config.go b/pkg/config/config.go index c3b083a5..c6617957 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -7,22 +7,223 @@ package config import ( "context" "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + "sync" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/sts" + + "github.com/platform-engineering-labs/formae/pkg/plugin" +) + +// Auth discriminator values, matching the Type field the Pkl schema renders +// on the nested Auth object. +const ( + AuthTypeDefaultChain = "DefaultChain" + AuthTypeOidc = "Oidc" ) +// deprecatedFlatProfileWarning is logged, once, whenever a target's flat +// Profile field is what produces a DefaultChain auth (as opposed to an +// explicit `auth = DefaultChainAuth { ... }` block). It is the only +// deprecation surface available for this: the plugin does not implement +// ObservablePlugin, so plugin.LoggerFromContext returns an indistinguishable +// no-op, and Pkl's @Deprecated annotation does not print at eval. +const deprecatedFlatProfileWarning = "target config uses the deprecated flat profile; set auth = new DefaultChainAuth" + +// warnFlatFallback fires the deprecation warning at most once for Config +// values that carry no OidcDeps (e.g. a plugin instance the agent hasn't +// wired a token source into), so flat-profile users still see it once per +// process rather than not at all. +var warnFlatFallback sync.Once + +// OidcDeps is owned by the Plugin instance, never process-global: a global +// would outlive individual plugin instances and make tests order-dependent. +// A Config with nil deps behaves flat-only: Oidc auth fails closed, and the +// deprecation warning falls back to warnFlatFallback. +type OidcDeps struct { + // Source mints the OIDC identity tokens exchanged for AWS credentials. + Source plugin.OidcTokenSource + + // caches holds one *aws.CredentialsCache per distinct Oidc auth block, + // built lazily. Populated starting with the credential composition + // work; declared now so the shape is settled. + caches sync.Map + + // stsFactory builds the STS client used for AssumeRoleWithWebIdentity. + // A seam for tests; production wiring is sts.NewFromConfig. + stsFactory func(aws.Config) stscreds.AssumeRoleWithWebIdentityAPIClient + + // warnFlat ensures the flat-profile deprecation warning logs at most + // once per plugin instance. + warnFlat sync.Once +} + +// NewOidcDeps builds the OidcDeps a Plugin instance owns, wired to mint AWS +// credentials against real STS. +func NewOidcDeps(src plugin.OidcTokenSource) *OidcDeps { + return &OidcDeps{ + Source: src, + stsFactory: func(cfg aws.Config) stscreds.AssumeRoleWithWebIdentityAPIClient { + return sts.NewFromConfig(cfg) + }, + } +} + +// oidcCredentials mints AWS credentials for roleArn via the OIDC token +// source and AssumeRoleWithWebIdentity. The actual composition (identity +// token retrieval, STS exchange, per-role caching) is not wired yet; this +// returns a clear error rather than ever falling back to ambient +// credentials. +func (d *OidcDeps) oidcCredentials(_ context.Context, _ string, roleArn string) (aws.CredentialsProvider, error) { + return nil, fmt.Errorf("config: Oidc auth credential minting for role %q is not yet implemented", roleArn) +} + type Config struct { - Region string `json:"Region"` - Profile string `json:"Profile"` + Region string `json:"Region"` + Profile string `json:"Profile"` + Auth json.RawMessage `json:"Auth,omitempty"` + + // deps carries what the plugin instance owns: the token source, the + // per-plugin credentials-cache registry, the STS client factory seam, + // and the warn-once state. Never serialized; nil deps means flat-only + // behavior. + deps *OidcDeps } -func (c *Config) ToAwsConfig(ctx context.Context) (aws.Config, error) { - var opts []func(*awsconfig.LoadOptions) error +// WithOidcDeps threads the plugin instance's OidcDeps onto Config without +// changing FromTargetConfig's signature or call sites. +func (c *Config) WithOidcDeps(d *OidcDeps) *Config { + c.deps = d + return c +} + +// authDiscriminator is the shape every Auth block variant shares. +type authDiscriminator struct { + Type string `json:"Type"` +} + +// isAuthAbsent reports whether raw carries no explicit Auth block: this is +// true for a nil/empty field, the JSON literal null, and whitespace-only +// content. +func isAuthAbsent(raw json.RawMessage) bool { + trimmed := strings.TrimSpace(string(raw)) + return trimmed == "" || trimmed == "null" +} + +// synthesizeDefaultChain builds the Auth block an absent Auth field implies: +// a DefaultChain carrying the flat Profile spelling. +func synthesizeDefaultChain(profile string) json.RawMessage { + raw, _ := json.Marshal(struct { + Type string `json:"Type"` + Profile string `json:"Profile"` + }{Type: AuthTypeDefaultChain, Profile: profile}) + return raw +} + +// effectiveAuth resolves the auth block that governs this Config: the +// explicit Auth block if the forma set one, or a synthesised DefaultChain +// carrying the flat Profile spelling otherwise. Setting both is rejected, +// mirroring the Pkl-level `this == null || profile == null` constraint so +// callers that bypass the schema (tests, an older formae binary) still get +// the rule enforced in Go. +func (c *Config) effectiveAuth() (string, json.RawMessage, error) { + if isAuthAbsent(c.Auth) { + return AuthTypeDefaultChain, synthesizeDefaultChain(c.Profile), nil + } - opts = append(opts, awsconfig.WithRegion(c.Region)) if c.Profile != "" { - opts = append(opts, awsconfig.WithSharedConfigProfile(c.Profile)) + return "", nil, errors.New("config: Auth and Profile are mutually exclusive; set one") + } + + var disc authDiscriminator + if err := json.Unmarshal(c.Auth, &disc); err != nil { + return "", nil, fmt.Errorf("config: malformed Auth block: %w", err) + } + if disc.Type == "" { + return "", nil, errors.New("config: Auth block is missing its Type discriminator") + } + + return disc.Type, c.Auth, nil +} + +// awsConfigOptions resolves this Config's effective auth into the +// awsconfig.LoadOptions functions ToAwsConfig hands to LoadDefaultConfig. +// Split out from ToAwsConfig so tests can inspect the resolved options (e.g. +// the profile threaded through) without exercising real credential/config- +// file resolution. +func (c *Config) awsConfigOptions(ctx context.Context) ([]func(*awsconfig.LoadOptions) error, error) { + synthesized := isAuthAbsent(c.Auth) + + authType, rawAuth, err := c.effectiveAuth() + if err != nil { + return nil, err + } + + opts := []func(*awsconfig.LoadOptions) error{awsconfig.WithRegion(c.Region)} + + switch authType { + case AuthTypeDefaultChain: + var chain struct { + Profile string `json:"Profile"` + } + if err := json.Unmarshal(rawAuth, &chain); err != nil { + return nil, fmt.Errorf("config: malformed DefaultChain auth block: %w", err) + } + if chain.Profile != "" { + opts = append(opts, awsconfig.WithSharedConfigProfile(chain.Profile)) + } + if synthesized && chain.Profile != "" { + c.warnDeprecatedFlatProfile() + } + + case AuthTypeOidc: + var oidc struct { + RoleArn string `json:"RoleArn"` + } + if err := json.Unmarshal(rawAuth, &oidc); err != nil { + return nil, fmt.Errorf("config: malformed Oidc auth block: %w", err) + } + if oidc.RoleArn == "" { + return nil, errors.New("config: Oidc auth requires RoleArn") + } + if c.deps == nil || c.deps.Source == nil { + return nil, errors.New("config: Oidc auth requires an OIDC token source, but this plugin instance has none wired (failing closed rather than falling back to ambient credentials)") + } + + creds, err := c.deps.oidcCredentials(ctx, c.Region, oidc.RoleArn) + if err != nil { + return nil, fmt.Errorf("config: resolving Oidc credentials: %w", err) + } + opts = append(opts, awsconfig.WithCredentialsProvider(creds)) + + default: + return nil, fmt.Errorf("config: unknown Auth type %q", authType) + } + + return opts, nil +} + +// warnDeprecatedFlatProfile logs deprecatedFlatProfileWarning once per +// plugin instance (via OidcDeps.warnFlat), or once per process when this +// Config carries no OidcDeps at all. +func (c *Config) warnDeprecatedFlatProfile() { + if c.deps != nil { + c.deps.warnFlat.Do(func() { slog.Warn(deprecatedFlatProfileWarning) }) + return + } + warnFlatFallback.Do(func() { slog.Warn(deprecatedFlatProfileWarning) }) +} + +func (c *Config) ToAwsConfig(ctx context.Context) (aws.Config, error) { + opts, err := c.awsConfigOptions(ctx) + if err != nil { + return aws.Config{}, err } return awsconfig.LoadDefaultConfig(ctx, opts...) @@ -38,4 +239,3 @@ func FromTargetConfig(targetConfig json.RawMessage) *Config { return config } - diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 00000000..48976ba1 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,304 @@ +// © 2025 Platform Engineering Labs Inc. +// +// SPDX-License-Identifier: FSL-1.1-ALv2 + +//go:build unit + +package config + +import ( + "context" + "encoding/json" + "log/slog" + "testing" + + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// countingHandler is a slog.Handler that counts the records routed to it, so +// a test can assert a warning fired a specific number of times without +// inspecting message text. +type countingHandler struct{ count *int } + +func (h countingHandler) Enabled(context.Context, slog.Level) bool { return true } +func (h countingHandler) Handle(context.Context, slog.Record) error { *h.count++; return nil } +func (h countingHandler) WithAttrs([]slog.Attr) slog.Handler { return h } +func (h countingHandler) WithGroup(string) slog.Handler { return h } + +// stubTokenSource is a plugin.OidcTokenSource that never actually gets +// called in these tests: it exists so a Config can carry non-nil OidcDeps +// with a non-nil Source, exercising the "wired but not yet implemented" +// path distinct from the "nothing wired at all" fail-closed path. +type stubTokenSource struct{} + +func (stubTokenSource) IdentityToken(context.Context, string) (string, error) { + return "stub-token", nil +} + +func TestEffectiveAuth(t *testing.T) { + tests := []struct { + name string + config Config + wantType string + wantAuth string // JSON to compare against, empty means "don't check" + wantErr string + }{ + { + name: "flat profile synthesises DefaultChain", + config: Config{Profile: "legacy-profile"}, + wantType: AuthTypeDefaultChain, + wantAuth: `{"Type":"DefaultChain","Profile":"legacy-profile"}`, + }, + { + name: "no profile and no auth synthesises DefaultChain with an empty profile", + config: Config{}, + wantType: AuthTypeDefaultChain, + wantAuth: `{"Type":"DefaultChain","Profile":""}`, + }, + { + name: "explicit DefaultChain passes through unchanged", + config: Config{Auth: json.RawMessage(`{"Type":"DefaultChain","Profile":"chain-profile"}`)}, + wantType: AuthTypeDefaultChain, + wantAuth: `{"Type":"DefaultChain","Profile":"chain-profile"}`, + }, + { + name: "explicit Oidc passes through unchanged", + config: Config{Auth: json.RawMessage(`{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}`)}, + wantType: AuthTypeOidc, + wantAuth: `{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}`, + }, + { + name: "explicit Auth and flat Profile both set is rejected", + config: Config{ + Profile: "legacy-profile", + Auth: json.RawMessage(`{"Type":"DefaultChain","Profile":"chain-profile"}`), + }, + wantErr: "mutually exclusive", + }, + { + name: "null Auth literal is treated as absent", + config: Config{Profile: "legacy-profile", Auth: json.RawMessage(`null`)}, + wantType: AuthTypeDefaultChain, + wantAuth: `{"Type":"DefaultChain","Profile":"legacy-profile"}`, + }, + { + name: "whitespace-only Auth is treated as absent", + config: Config{Profile: "legacy-profile", Auth: json.RawMessage(" \n\t")}, + wantType: AuthTypeDefaultChain, + wantAuth: `{"Type":"DefaultChain","Profile":"legacy-profile"}`, + }, + { + name: "malformed Auth JSON errors", + config: Config{Auth: json.RawMessage(`{not valid json`)}, + wantErr: "malformed Auth block", + }, + { + name: "Auth object missing its Type discriminator errors", + config: Config{Auth: json.RawMessage(`{"RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}`)}, + wantErr: "missing its Type discriminator", + }, + { + name: "empty Auth object errors", + config: Config{Auth: json.RawMessage(`{}`)}, + wantErr: "missing its Type discriminator", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotType, gotAuth, err := tt.config.effectiveAuth() + + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantType, gotType) + assert.JSONEq(t, tt.wantAuth, string(gotAuth)) + }) + } +} + +func TestToAwsConfig_UnknownAuthType(t *testing.T) { + c := &Config{Region: "us-east-1", Auth: json.RawMessage(`{"Type":"Bogus"}`)} + + _, err := c.ToAwsConfig(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown Auth type") +} + +func TestToAwsConfig_OidcMissingRoleArn(t *testing.T) { + for _, auth := range []string{ + `{"Type":"Oidc"}`, + `{"Type":"Oidc","RoleArn":""}`, + } { + c := &Config{Region: "us-east-1", Auth: json.RawMessage(auth)} + + _, err := c.ToAwsConfig(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "requires RoleArn") + } +} + +func TestToAwsConfig_OidcWithNoDepsFailsClosed(t *testing.T) { + c := &Config{ + Region: "us-east-1", + Auth: json.RawMessage(`{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}`), + } + + _, err := c.ToAwsConfig(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "OIDC token source") +} + +func TestToAwsConfig_OidcWithDepsButNilSourceFailsClosed(t *testing.T) { + c := (&Config{ + Region: "us-east-1", + Auth: json.RawMessage(`{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}`), + }).WithOidcDeps(&OidcDeps{}) + + _, err := c.ToAwsConfig(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "OIDC token source") +} + +func TestToAwsConfig_OidcWithDepsWiredButNotYetImplemented(t *testing.T) { + c := (&Config{ + Region: "us-east-1", + Auth: json.RawMessage(`{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}`), + }).WithOidcDeps(NewOidcDeps(stubTokenSource{})) + + _, err := c.ToAwsConfig(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "not yet implemented") +} + +func TestAwsConfigOptions_FlatProfileAppliesSharedConfigProfile(t *testing.T) { + // Carries its own OidcDeps purely so the deprecation warning this path + // triggers lands on a per-test sync.Once rather than the process-wide + // warnFlatFallback, which TestDeprecationWarning_FallsBackToProcessOnceWhenDepsAreNil + // owns exclusively for the life of the test binary. + c := (&Config{Region: "us-east-1", Profile: "legacy-profile"}).WithOidcDeps(NewOidcDeps(nil)) + + optFns, err := c.awsConfigOptions(context.Background()) + require.NoError(t, err) + + var lo awsconfig.LoadOptions + for _, fn := range optFns { + require.NoError(t, fn(&lo)) + } + + assert.Equal(t, "us-east-1", lo.Region) + assert.Equal(t, "legacy-profile", lo.SharedConfigProfile) +} + +func TestAwsConfigOptions_NoProfileLeavesSharedConfigProfileUnset(t *testing.T) { + c := &Config{Region: "us-east-1"} + + optFns, err := c.awsConfigOptions(context.Background()) + require.NoError(t, err) + + var lo awsconfig.LoadOptions + for _, fn := range optFns { + require.NoError(t, fn(&lo)) + } + + assert.Equal(t, "us-east-1", lo.Region) + assert.Empty(t, lo.SharedConfigProfile) +} + +func TestAwsConfigOptions_ExplicitDefaultChainAppliesItsProfile(t *testing.T) { + c := &Config{ + Region: "us-east-1", + Auth: json.RawMessage(`{"Type":"DefaultChain","Profile":"chain-profile"}`), + } + + optFns, err := c.awsConfigOptions(context.Background()) + require.NoError(t, err) + + var lo awsconfig.LoadOptions + for _, fn := range optFns { + require.NoError(t, fn(&lo)) + } + + assert.Equal(t, "chain-profile", lo.SharedConfigProfile) +} + +// withCountingSlogDefault swaps the package-level slog default for the +// duration of the test, restoring it on cleanup, and returns a pointer to +// the record count the swapped-in handler increments. +func withCountingSlogDefault(t *testing.T) *int { + t.Helper() + + count := 0 + prev := slog.Default() + slog.SetDefault(slog.New(countingHandler{count: &count})) + t.Cleanup(func() { slog.SetDefault(prev) }) + + return &count +} + +func TestDeprecationWarning_FiresOncePerOidcDeps(t *testing.T) { + count := withCountingSlogDefault(t) + + deps := NewOidcDeps(nil) + c1 := (&Config{Region: "us-east-1", Profile: "legacy-profile"}).WithOidcDeps(deps) + c2 := (&Config{Region: "us-east-1", Profile: "legacy-profile"}).WithOidcDeps(deps) + + _, err := c1.awsConfigOptions(context.Background()) + require.NoError(t, err) + _, err = c2.awsConfigOptions(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 1, *count) +} + +func TestDeprecationWarning_DoesNotFireForExplicitDefaultChainProfile(t *testing.T) { + count := withCountingSlogDefault(t) + + deps := NewOidcDeps(nil) + c := (&Config{ + Region: "us-east-1", + Auth: json.RawMessage(`{"Type":"DefaultChain","Profile":"chain-profile"}`), + }).WithOidcDeps(deps) + + _, err := c.awsConfigOptions(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 0, *count) +} + +func TestDeprecationWarning_DoesNotFireWhenNoProfileIsSet(t *testing.T) { + count := withCountingSlogDefault(t) + + deps := NewOidcDeps(nil) + c := (&Config{Region: "us-east-1"}).WithOidcDeps(deps) + + _, err := c.awsConfigOptions(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 0, *count) +} + +func TestDeprecationWarning_FallsBackToProcessOnceWhenDepsAreNil(t *testing.T) { + count := withCountingSlogDefault(t) + + c1 := &Config{Region: "us-east-1", Profile: "legacy-profile"} + c2 := &Config{Region: "us-east-1", Profile: "legacy-profile"} + + _, err := c1.awsConfigOptions(context.Background()) + require.NoError(t, err) + _, err = c2.awsConfigOptions(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 1, *count) +} From 0a807d08e70b0445c9fd91ad6debc14aac3857b7 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:24:11 -0700 Subject: [PATCH 05/13] chore: annotate the not-yet-consumed cache field for the linter OidcDeps.caches has no reader until the follow-up credential-composition work lands, so golangci-lint's unused check flags it. The field's shape is mandated by the auth plan and gains its consumer next, so suppress rather than delete it. --- pkg/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index c6617957..992e4967 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -53,7 +53,7 @@ type OidcDeps struct { // caches holds one *aws.CredentialsCache per distinct Oidc auth block, // built lazily. Populated starting with the credential composition // work; declared now so the shape is settled. - caches sync.Map + caches sync.Map //nolint:unused // populated by the follow-up credential-composition task // stsFactory builds the STS client used for AssumeRoleWithWebIdentity. // A seam for tests; production wiring is sts.NewFromConfig. From 331601a5062e653ff9ad6aa796e6d028079f38cb Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:31:18 -0700 Subject: [PATCH 06/13] feat(config): exchange brokered identity tokens through a keyed credentials cache --- pkg/config/config.go | 26 +-- pkg/config/config_test.go | 41 ++-- pkg/config/oidc_credentials.go | 142 +++++++++++++ pkg/config/oidc_credentials_test.go | 312 ++++++++++++++++++++++++++++ 4 files changed, 485 insertions(+), 36 deletions(-) create mode 100644 pkg/config/oidc_credentials.go create mode 100644 pkg/config/oidc_credentials_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 992e4967..290cb227 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -51,9 +51,8 @@ type OidcDeps struct { Source plugin.OidcTokenSource // caches holds one *aws.CredentialsCache per distinct Oidc auth block, - // built lazily. Populated starting with the credential composition - // work; declared now so the shape is settled. - caches sync.Map //nolint:unused // populated by the follow-up credential-composition task + // built lazily by credentialsCacheFor. + caches sync.Map // stsFactory builds the STS client used for AssumeRoleWithWebIdentity. // A seam for tests; production wiring is sts.NewFromConfig. @@ -75,15 +74,6 @@ func NewOidcDeps(src plugin.OidcTokenSource) *OidcDeps { } } -// oidcCredentials mints AWS credentials for roleArn via the OIDC token -// source and AssumeRoleWithWebIdentity. The actual composition (identity -// token retrieval, STS exchange, per-role caching) is not wired yet; this -// returns a clear error rather than ever falling back to ambient -// credentials. -func (d *OidcDeps) oidcCredentials(_ context.Context, _ string, roleArn string) (aws.CredentialsProvider, error) { - return nil, fmt.Errorf("config: Oidc auth credential minting for role %q is not yet implemented", roleArn) -} - type Config struct { Region string `json:"Region"` Profile string `json:"Profile"` @@ -157,7 +147,7 @@ func (c *Config) effectiveAuth() (string, json.RawMessage, error) { // Split out from ToAwsConfig so tests can inspect the resolved options (e.g. // the profile threaded through) without exercising real credential/config- // file resolution. -func (c *Config) awsConfigOptions(ctx context.Context) ([]func(*awsconfig.LoadOptions) error, error) { +func (c *Config) awsConfigOptions() ([]func(*awsconfig.LoadOptions) error, error) { synthesized := isAuthAbsent(c.Auth) authType, rawAuth, err := c.effectiveAuth() @@ -196,11 +186,9 @@ func (c *Config) awsConfigOptions(ctx context.Context) ([]func(*awsconfig.LoadOp return nil, errors.New("config: Oidc auth requires an OIDC token source, but this plugin instance has none wired (failing closed rather than falling back to ambient credentials)") } - creds, err := c.deps.oidcCredentials(ctx, c.Region, oidc.RoleArn) - if err != nil { - return nil, fmt.Errorf("config: resolving Oidc credentials: %w", err) - } - opts = append(opts, awsconfig.WithCredentialsProvider(creds)) + opts = append(opts, awsconfig.WithCredentialsProvider( + c.deps.oidcCredentials(c.Region, oidc.RoleArn, rawAuth), + )) default: return nil, fmt.Errorf("config: unknown Auth type %q", authType) @@ -221,7 +209,7 @@ func (c *Config) warnDeprecatedFlatProfile() { } func (c *Config) ToAwsConfig(ctx context.Context) (aws.Config, error) { - opts, err := c.awsConfigOptions(ctx) + opts, err := c.awsConfigOptions() if err != nil { return aws.Config{}, err } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 48976ba1..cce4566a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -12,6 +12,7 @@ import ( "log/slog" "testing" + "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -27,10 +28,10 @@ func (h countingHandler) Handle(context.Context, slog.Record) error { *h.count++ func (h countingHandler) WithAttrs([]slog.Attr) slog.Handler { return h } func (h countingHandler) WithGroup(string) slog.Handler { return h } -// stubTokenSource is a plugin.OidcTokenSource that never actually gets -// called in these tests: it exists so a Config can carry non-nil OidcDeps -// with a non-nil Source, exercising the "wired but not yet implemented" -// path distinct from the "nothing wired at all" fail-closed path. +// stubTokenSource is a plugin.OidcTokenSource that always mints the same +// token, so a Config can carry non-nil OidcDeps with a non-nil Source and +// exercise the wired path rather than the "nothing wired at all" fail-closed +// path. type stubTokenSource struct{} func (stubTokenSource) IdentityToken(context.Context, string) (string, error) { @@ -170,16 +171,22 @@ func TestToAwsConfig_OidcWithDepsButNilSourceFailsClosed(t *testing.T) { assert.Contains(t, err.Error(), "OIDC token source") } -func TestToAwsConfig_OidcWithDepsWiredButNotYetImplemented(t *testing.T) { +func TestAwsConfigOptions_OidcAppliesACredentialsCache(t *testing.T) { c := (&Config{ Region: "us-east-1", Auth: json.RawMessage(`{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}`), }).WithOidcDeps(NewOidcDeps(stubTokenSource{})) - _, err := c.ToAwsConfig(context.Background()) + optFns, err := c.awsConfigOptions() + require.NoError(t, err) - require.Error(t, err) - assert.Contains(t, err.Error(), "not yet implemented") + var lo awsconfig.LoadOptions + for _, fn := range optFns { + require.NoError(t, fn(&lo)) + } + + assert.Equal(t, "us-east-1", lo.Region) + assert.IsType(t, &aws.CredentialsCache{}, lo.Credentials) } func TestAwsConfigOptions_FlatProfileAppliesSharedConfigProfile(t *testing.T) { @@ -189,7 +196,7 @@ func TestAwsConfigOptions_FlatProfileAppliesSharedConfigProfile(t *testing.T) { // owns exclusively for the life of the test binary. c := (&Config{Region: "us-east-1", Profile: "legacy-profile"}).WithOidcDeps(NewOidcDeps(nil)) - optFns, err := c.awsConfigOptions(context.Background()) + optFns, err := c.awsConfigOptions() require.NoError(t, err) var lo awsconfig.LoadOptions @@ -204,7 +211,7 @@ func TestAwsConfigOptions_FlatProfileAppliesSharedConfigProfile(t *testing.T) { func TestAwsConfigOptions_NoProfileLeavesSharedConfigProfileUnset(t *testing.T) { c := &Config{Region: "us-east-1"} - optFns, err := c.awsConfigOptions(context.Background()) + optFns, err := c.awsConfigOptions() require.NoError(t, err) var lo awsconfig.LoadOptions @@ -222,7 +229,7 @@ func TestAwsConfigOptions_ExplicitDefaultChainAppliesItsProfile(t *testing.T) { Auth: json.RawMessage(`{"Type":"DefaultChain","Profile":"chain-profile"}`), } - optFns, err := c.awsConfigOptions(context.Background()) + optFns, err := c.awsConfigOptions() require.NoError(t, err) var lo awsconfig.LoadOptions @@ -254,9 +261,9 @@ func TestDeprecationWarning_FiresOncePerOidcDeps(t *testing.T) { c1 := (&Config{Region: "us-east-1", Profile: "legacy-profile"}).WithOidcDeps(deps) c2 := (&Config{Region: "us-east-1", Profile: "legacy-profile"}).WithOidcDeps(deps) - _, err := c1.awsConfigOptions(context.Background()) + _, err := c1.awsConfigOptions() require.NoError(t, err) - _, err = c2.awsConfigOptions(context.Background()) + _, err = c2.awsConfigOptions() require.NoError(t, err) assert.Equal(t, 1, *count) @@ -271,7 +278,7 @@ func TestDeprecationWarning_DoesNotFireForExplicitDefaultChainProfile(t *testing Auth: json.RawMessage(`{"Type":"DefaultChain","Profile":"chain-profile"}`), }).WithOidcDeps(deps) - _, err := c.awsConfigOptions(context.Background()) + _, err := c.awsConfigOptions() require.NoError(t, err) assert.Equal(t, 0, *count) @@ -283,7 +290,7 @@ func TestDeprecationWarning_DoesNotFireWhenNoProfileIsSet(t *testing.T) { deps := NewOidcDeps(nil) c := (&Config{Region: "us-east-1"}).WithOidcDeps(deps) - _, err := c.awsConfigOptions(context.Background()) + _, err := c.awsConfigOptions() require.NoError(t, err) assert.Equal(t, 0, *count) @@ -295,9 +302,9 @@ func TestDeprecationWarning_FallsBackToProcessOnceWhenDepsAreNil(t *testing.T) { c1 := &Config{Region: "us-east-1", Profile: "legacy-profile"} c2 := &Config{Region: "us-east-1", Profile: "legacy-profile"} - _, err := c1.awsConfigOptions(context.Background()) + _, err := c1.awsConfigOptions() require.NoError(t, err) - _, err = c2.awsConfigOptions(context.Background()) + _, err = c2.awsConfigOptions() require.NoError(t, err) assert.Equal(t, 1, *count) diff --git a/pkg/config/oidc_credentials.go b/pkg/config/oidc_credentials.go new file mode 100644 index 00000000..33c46ec5 --- /dev/null +++ b/pkg/config/oidc_credentials.go @@ -0,0 +1,142 @@ +// © 2025 Platform Engineering Labs Inc. +// +// SPDX-License-Identifier: FSL-1.1-ALv2 + +package config + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + + "github.com/platform-engineering-labs/formae/pkg/plugin" +) + +// oidcAudience is the audience every identity token destined for AWS is +// minted for; the role's trust policy matches on it. +const oidcAudience = "sts.amazonaws.com" + +// oidcRefreshTimeout bounds one credential refresh end to end: minting an +// identity token plus exchanging it at STS. The stock aws.CredentialsCache +// hands the refresh a context whose cancellation is already suppressed, so +// without a bound of our own a wedged broker would hold the refresh open +// indefinitely. +const oidcRefreshTimeout = 30 * time.Second + +// oneShotRetriever hands the stock, context-free IdentityTokenRetriever a +// token that was already minted for exactly one Retrieve call. Tokens are +// short-lived and single-use by design, so a retriever is never reused. +type oneShotRetriever struct{ token []byte } + +func (r oneShotRetriever) GetIdentityToken() ([]byte, error) { return r.token, nil } + +// oidcCredentialsProvider mints a fresh identity token per refresh and +// exchanges it for AWS credentials by assuming roleArn. It holds no +// credential state: aws.CredentialsCache owns caching and refresh timing. +type oidcCredentialsProvider struct { + source plugin.OidcTokenSource + roleArn string + stsClient stscreds.AssumeRoleWithWebIdentityAPIClient +} + +// Retrieve reads nothing mutable from ctx. aws.CredentialsCache already +// suppresses caller cancellation on the refresh path (values survive, +// cancellation does not), so honouring the incoming deadline here would be an +// illusion; the refresh instead runs under its own bounded context derived +// with context.WithoutCancel, which keeps the request-scoped values the token +// source needs to reach the right broker. +func (p *oidcCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { + refreshCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), oidcRefreshTimeout) + defer cancel() + + token, err := p.source.IdentityToken(refreshCtx, oidcAudience) + if err != nil { + return aws.Credentials{}, fmt.Errorf("config: minting an identity token for role %q: %w", p.roleArn, err) + } + + exchange := stscreds.NewWebIdentityRoleProvider(p.stsClient, p.roleArn, oneShotRetriever{token: []byte(token)}) + + creds, err := exchange.Retrieve(refreshCtx) + if err != nil { + return aws.Credentials{}, fmt.Errorf( + "config: exchanging the identity token for credentials on role %q: %w", + p.roleArn, redactToken(err, token), + ) + } + + return creds, nil +} + +// redactToken strips any verbatim occurrence of the identity token from an +// error's message. STS echoes the submitted token in some rejection bodies, +// and the token is bearer credential material that must never reach a log or +// an error shown to a user. Redacting flattens the error chain, so it only +// happens when a leak is actually present. +func redactToken(err error, token string) error { + if token == "" { + return err + } + + msg := err.Error() + if !strings.Contains(msg, token) { + return err + } + + return errors.New(strings.ReplaceAll(msg, token, "[redacted identity token]")) +} + +// oidcCacheKey identifies one credentials cache. Two targets share a cache +// only when they assume the same role in the same region under a +// byte-identical auth block, so any auth change (a new session name, a new +// policy) yields a distinct cache rather than silently reusing credentials +// minted under the old settings. The NUL separators keep a boundary shift +// between the role and the region from colliding. +func oidcCacheKey(roleArn, region string, rawAuth json.RawMessage) string { + sum := sha256.Sum256(rawAuth) + + return roleArn + "\x00" + region + "\x00" + hex.EncodeToString(sum[:]) +} + +// credentialsCacheHolder carries a lazily built cache plus the once that +// guarantees a concurrent miss constructs it exactly one time. +type credentialsCacheHolder struct { + once sync.Once + cache *aws.CredentialsCache +} + +// credentialsCacheFor returns the cache registered under key, building it on +// first use. There is deliberately no eviction: the map grows with the number +// of distinct auth configurations a plugin instance sees, which is bounded by +// the targets in play, and evicting an entry could not invalidate the +// references already handed out, so it would reintroduce exactly the +// duplicate token exchanges the cache exists to prevent. +func (d *OidcDeps) credentialsCacheFor(key string, build func() *aws.CredentialsCache) *aws.CredentialsCache { + entry, _ := d.caches.LoadOrStore(key, &credentialsCacheHolder{}) + holder := entry.(*credentialsCacheHolder) + holder.once.Do(func() { holder.cache = build() }) + + return holder.cache +} + +// oidcCredentials returns the plugin-lifetime credentials cache that mints +// AWS credentials for roleArn by exchanging brokered identity tokens. The STS +// client is built from a region-only base config: it needs no credentials of +// its own, because AssumeRoleWithWebIdentity is an unsigned call. +func (d *OidcDeps) oidcCredentials(region, roleArn string, rawAuth json.RawMessage) *aws.CredentialsCache { + return d.credentialsCacheFor(oidcCacheKey(roleArn, region, rawAuth), func() *aws.CredentialsCache { + return aws.NewCredentialsCache(&oidcCredentialsProvider{ + source: d.Source, + roleArn: roleArn, + stsClient: d.stsFactory(aws.Config{Region: region}), + }) + }) +} diff --git a/pkg/config/oidc_credentials_test.go b/pkg/config/oidc_credentials_test.go new file mode 100644 index 00000000..0f922c17 --- /dev/null +++ b/pkg/config/oidc_credentials_test.go @@ -0,0 +1,312 @@ +// © 2025 Platform Engineering Labs Inc. +// +// SPDX-License-Identifier: FSL-1.1-ALv2 + +//go:build unit + +package config + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "path/filepath" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/sts" + ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testRoleArn = "arn:aws:iam::123456789012:role/formae-agent" + +// tokenSourceFunc adapts a function to plugin.OidcTokenSource so a test can +// script what the broker answers per call. +type tokenSourceFunc func(ctx context.Context, audience string) (string, error) + +func (f tokenSourceFunc) IdentityToken(ctx context.Context, audience string) (string, error) { + return f(ctx, audience) +} + +// fakeSTS stands in for the AssumeRoleWithWebIdentity API client, recording +// what each exchange was called with and answering with canned credentials. +type fakeSTS struct { + mu sync.Mutex + + calls int + token string + roleArn string + ctxErr error + hadDeadline bool + remaining time.Duration + + // fail, when set, turns the exchange into the error it returns. It + // receives the submitted token so a test can make the failure echo it. + fail func(token string) error +} + +func (f *fakeSTS) AssumeRoleWithWebIdentity( + ctx context.Context, + in *sts.AssumeRoleWithWebIdentityInput, + _ ...func(*sts.Options), +) (*sts.AssumeRoleWithWebIdentityOutput, error) { + f.mu.Lock() + defer f.mu.Unlock() + + f.calls++ + f.token = aws.ToString(in.WebIdentityToken) + f.roleArn = aws.ToString(in.RoleArn) + f.ctxErr = ctx.Err() + if deadline, ok := ctx.Deadline(); ok { + f.hadDeadline = true + f.remaining = time.Until(deadline) + } + + if f.fail != nil { + if err := f.fail(f.token); err != nil { + return nil, err + } + } + + expires := time.Now().Add(time.Hour) + + return &sts.AssumeRoleWithWebIdentityOutput{ + Credentials: &ststypes.Credentials{ + AccessKeyId: aws.String("AKIAEXAMPLE"), + SecretAccessKey: aws.String("example-secret"), + SessionToken: aws.String("example-session"), + Expiration: &expires, + }, + AssumedRoleUser: &ststypes.AssumedRoleUser{ + Arn: aws.String("arn:aws:sts::123456789012:assumed-role/formae-agent/session"), + }, + }, nil +} + +func (f *fakeSTS) callCount() int { + f.mu.Lock() + defer f.mu.Unlock() + + return f.calls +} + +// isolateAwsEnv points the SDK's shared-config discovery at empty files in a +// temp dir, so a test that runs LoadDefaultConfig does not pick up whatever +// AWS configuration the developer's machine happens to carry. +func isolateAwsEnv(t *testing.T) { + t.Helper() + + dir := t.TempDir() + t.Setenv("AWS_CONFIG_FILE", filepath.Join(dir, "config")) + t.Setenv("AWS_SHARED_CREDENTIALS_FILE", filepath.Join(dir, "credentials")) + t.Setenv("AWS_PROFILE", "") + t.Setenv("AWS_EC2_METADATA_DISABLED", "true") +} + +func TestRetrieve_ExchangesTheSourceTokenForCredentials(t *testing.T) { + var gotAudience string + source := tokenSourceFunc(func(_ context.Context, audience string) (string, error) { + gotAudience = audience + return "stub-token", nil + }) + fake := &fakeSTS{} + provider := &oidcCredentialsProvider{source: source, roleArn: testRoleArn, stsClient: fake} + + creds, err := provider.Retrieve(context.Background()) + require.NoError(t, err) + + assert.Equal(t, "sts.amazonaws.com", gotAudience) + assert.Equal(t, 1, fake.callCount()) + assert.Equal(t, "stub-token", fake.token) + assert.Equal(t, testRoleArn, fake.roleArn) + assert.Equal(t, "AKIAEXAMPLE", creds.AccessKeyID) + assert.Equal(t, "example-secret", creds.SecretAccessKey) + assert.Equal(t, "example-session", creds.SessionToken) + assert.True(t, creds.CanExpire) + assert.WithinDuration(t, time.Now().Add(time.Hour), creds.Expires, time.Minute) +} + +func TestRetrieve_SourceErrorFailsClosed(t *testing.T) { + source := tokenSourceFunc(func(context.Context, string) (string, error) { + return "", errors.New("broker unavailable") + }) + fake := &fakeSTS{} + provider := &oidcCredentialsProvider{source: source, roleArn: testRoleArn, stsClient: fake} + + creds, err := provider.Retrieve(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "broker unavailable") + assert.Equal(t, 0, fake.callCount()) + assert.Equal(t, aws.Credentials{}, creds) +} + +type routingKey struct{} + +func TestRetrieve_DerivesABoundedRefreshCtx(t *testing.T) { + // The incoming ctx is cancelled before Retrieve is even called: the + // refresh must still complete, and must still see the ctx's values. + incoming, cancel := context.WithCancel(context.WithValue(context.Background(), routingKey{}, "routed")) + cancel() + + var sourceErr error + var sourceValue any + source := tokenSourceFunc(func(ctx context.Context, _ string) (string, error) { + sourceErr = ctx.Err() + sourceValue = ctx.Value(routingKey{}) + return "stub-token", nil + }) + fake := &fakeSTS{} + provider := &oidcCredentialsProvider{source: source, roleArn: testRoleArn, stsClient: fake} + + creds, err := provider.Retrieve(incoming) + require.NoError(t, err) + assert.Equal(t, "AKIAEXAMPLE", creds.AccessKeyID) + + assert.NoError(t, sourceErr) + assert.Equal(t, "routed", sourceValue) + + assert.NoError(t, fake.ctxErr) + assert.True(t, fake.hadDeadline, "the refresh ctx must carry a deadline") + assert.Greater(t, fake.remaining, 25*time.Second) + assert.LessOrEqual(t, fake.remaining, oidcRefreshTimeout) +} + +func TestRetrieve_NeverLeaksTheTokenInErrors(t *testing.T) { + const secret = "header.super-secret-jwt-payload.signature" + + t.Run("an STS failure that echoes the submitted token", func(t *testing.T) { + source := tokenSourceFunc(func(context.Context, string) (string, error) { + return secret, nil + }) + fake := &fakeSTS{fail: func(token string) error { + return fmt.Errorf("InvalidIdentityToken: the token %s was rejected", token) + }} + provider := &oidcCredentialsProvider{source: source, roleArn: testRoleArn, stsClient: fake} + + _, err := provider.Retrieve(context.Background()) + + require.Error(t, err) + assert.NotContains(t, err.Error(), secret) + assert.Contains(t, err.Error(), "redacted") + }) + + t.Run("a source failure on a later refresh", func(t *testing.T) { + var calls int + source := tokenSourceFunc(func(context.Context, string) (string, error) { + calls++ + if calls == 1 { + return secret, nil + } + return "", errors.New("broker rotated its signing key") + }) + provider := &oidcCredentialsProvider{source: source, roleArn: testRoleArn, stsClient: &fakeSTS{}} + + _, err := provider.Retrieve(context.Background()) + require.NoError(t, err) + + _, err = provider.Retrieve(context.Background()) + + require.Error(t, err) + assert.NotContains(t, err.Error(), secret) + }) +} + +func TestCredentialsCacheFor_GetOrCreateIsSynchronized(t *testing.T) { + deps := NewOidcDeps(stubTokenSource{}) + + var builds int64 + build := func() *aws.CredentialsCache { + atomic.AddInt64(&builds, 1) + return aws.NewCredentialsCache(&oidcCredentialsProvider{ + source: stubTokenSource{}, + roleArn: testRoleArn, + stsClient: &fakeSTS{}, + }) + } + + const goroutines = 8 + got := make([]*aws.CredentialsCache, goroutines) + start := make(chan struct{}) + + var wg sync.WaitGroup + for i := range got { + wg.Add(1) + go func(i int) { + defer wg.Done() + <-start + got[i] = deps.credentialsCacheFor("shared-key", build) + }(i) + } + close(start) + wg.Wait() + + assert.EqualValues(t, 1, atomic.LoadInt64(&builds), "a concurrent miss must construct exactly once") + for _, cache := range got { + assert.Same(t, got[0], cache) + } +} + +func TestCredentialsCacheFor_KeyIncludesAuthHash(t *testing.T) { + deps := NewOidcDeps(stubTokenSource{}) + build := func() *aws.CredentialsCache { + return aws.NewCredentialsCache(&oidcCredentialsProvider{ + source: stubTokenSource{}, + roleArn: testRoleArn, + stsClient: &fakeSTS{}, + }) + } + + first := oidcCacheKey(testRoleArn, "us-east-1", json.RawMessage(`{"Type":"Oidc","RoleArn":"`+testRoleArn+`","SessionName":"a"}`)) + second := oidcCacheKey(testRoleArn, "us-east-1", json.RawMessage(`{"Type":"Oidc","RoleArn":"`+testRoleArn+`","SessionName":"b"}`)) + require.NotEqual(t, first, second) + + assert.Same(t, deps.credentialsCacheFor(first, build), deps.credentialsCacheFor(first, build)) + assert.NotSame(t, deps.credentialsCacheFor(first, build), deps.credentialsCacheFor(second, build)) +} + +func TestCredentialsCacheFor_KeySeparatesRoleAndRegion(t *testing.T) { + raw := json.RawMessage(`{"Type":"Oidc","RoleArn":"` + testRoleArn + `"}`) + + assert.NotEqual(t, + oidcCacheKey(testRoleArn, "us-east-1", raw), + oidcCacheKey(testRoleArn, "eu-west-1", raw), + ) + // The NUL separator keeps a role/region boundary shift from colliding. + assert.NotEqual(t, + oidcCacheKey("role", "a-b", raw), + oidcCacheKey("role-a", "b", raw), + ) +} + +func TestCacheReuse_OneExchangePerLifetime(t *testing.T) { + isolateAwsEnv(t) + + fake := &fakeSTS{} + deps := NewOidcDeps(stubTokenSource{}) + deps.stsFactory = func(aws.Config) stscreds.AssumeRoleWithWebIdentityAPIClient { return fake } + + auth := json.RawMessage(`{"Type":"Oidc","RoleArn":"` + testRoleArn + `"}`) + + for range 2 { + // A fresh Config each time, as every plugin operation builds one; + // only the plugin-lifetime deps are shared. + c := (&Config{Region: "us-east-1", Auth: auth}).WithOidcDeps(deps) + + cfg, err := c.ToAwsConfig(context.Background()) + require.NoError(t, err) + + creds, err := cfg.Credentials.Retrieve(context.Background()) + require.NoError(t, err) + assert.Equal(t, "AKIAEXAMPLE", creds.AccessKeyID) + } + + assert.Equal(t, 1, fake.callCount(), "the cached credentials must survive across ToAwsConfig calls") +} From 40103004694df10ad94172aec71ff7dac497f5db Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:49:33 -0700 Subject: [PATCH 07/13] feat: receive the oidc token source and thread it to target configs Plugin now implements plugin.OidcAware: SetOidcTokenSource wires an OidcDeps (backed by the production STS factory) onto the plugin instance, and every FromTargetConfig call site threads it onto the parsed Config via WithOidcDeps before the config is used. A nil deps value (no broker paired, or an old agent) flows through unchanged: WithOidcDeps(nil) is a no-op and Oidc auth keeps failing closed rather than falling back to ambient credentials. Also documents NewOidcDeps as the required constructor (a bare OidcDeps{} literal with Source set panics on first Oidc use, since stsFactory is left nil), and notes why oidcCacheKey omits the token source's identity. --- aws.go | 48 +++++++++++++++++------ aws_test.go | 71 ++++++++++++++++++++++++++++++++++ go.sum | 45 +++++++++++++++++++++ pkg/config/config.go | 5 +++ pkg/config/oidc_credentials.go | 5 +++ 5 files changed, 162 insertions(+), 12 deletions(-) diff --git a/aws.go b/aws.go index e184f6db..2fc0003d 100644 --- a/aws.go +++ b/aws.go @@ -29,11 +29,30 @@ import ( // The SDK automatically provides identity methods (Name, Version, Namespace) // and schema methods (SupportedResources, SchemaForResourceType) by reading // formae-plugin.pkl and schema/pkl/ at startup. -type Plugin struct{} +type Plugin struct { + // oidc carries the token source the SDK installs via SetOidcTokenSource, + // plus the plugin-lifetime credentials cache it backs. Nil until the SDK + // calls SetOidcTokenSource (or on an agent too old to pair a broker), in + // which case every target config threads nil deps and Oidc auth fails + // closed rather than falling back to ambient credentials. + oidc *config.OidcDeps +} // Compile-time check: Plugin must satisfy ResourcePlugin interface. var _ plugin.ResourcePlugin = &Plugin{} +// Compile-time check: Plugin must satisfy OidcAware, so the SDK hands it an +// OidcTokenSource at startup. +var _ plugin.OidcAware = &Plugin{} + +// SetOidcTokenSource receives the token source the SDK mints OIDC identity +// tokens through. Called once at startup; every FromTargetConfig call below +// threads the resulting deps onto the parsed Config so Oidc auth blocks can +// exchange a token for AWS credentials. +func (p *Plugin) SetOidcTokenSource(src plugin.OidcTokenSource) { + p.oidc = config.NewOidcDeps(src) +} + // EKSAutomodeResourceTypes lists AWS CloudFormation resource types that EKS Automode manages. // These resources are tagged with "kubernetes.io/cluster/" = "owned". var EKSAutomodeResourceTypes = []string{ @@ -157,7 +176,7 @@ func (p *Plugin) LabelConfig() pkgmodel.LabelConfig { } func (p *Plugin) Create(ctx context.Context, request *resource.CreateRequest) (*resource.CreateResult, error) { - targetConfig := config.FromTargetConfig(request.TargetConfig) + targetConfig := config.FromTargetConfig(request.TargetConfig).WithOidcDeps(p.oidc) if registry.HasProvisioner(request.ResourceType, resource.OperationCreate) { provisioner := registry.Get(request.ResourceType, resource.OperationCreate, targetConfig) return provisioner.Create(ctx, request) @@ -172,12 +191,13 @@ func (p *Plugin) Create(ctx context.Context, request *resource.CreateRequest) (* } func (p *Plugin) Update(ctx context.Context, request *resource.UpdateRequest) (*resource.UpdateResult, error) { + targetConfig := config.FromTargetConfig(request.TargetConfig).WithOidcDeps(p.oidc) if registry.HasProvisioner(request.ResourceType, resource.OperationUpdate) { - provisioner := registry.Get(request.ResourceType, resource.OperationUpdate, config.FromTargetConfig(request.TargetConfig)) + provisioner := registry.Get(request.ResourceType, resource.OperationUpdate, targetConfig) return provisioner.Update(ctx, request) } - client, err := ccx.NewClient(config.FromTargetConfig(request.TargetConfig)) + client, err := ccx.NewClient(targetConfig) if err != nil { return nil, err } @@ -186,14 +206,15 @@ func (p *Plugin) Update(ctx context.Context, request *resource.UpdateRequest) (* } func (p *Plugin) Status(ctx context.Context, request *resource.StatusRequest) (*resource.StatusResult, error) { + targetConfig := config.FromTargetConfig(request.TargetConfig).WithOidcDeps(p.oidc) if request.ResourceType != "" { if registry.HasProvisioner(request.ResourceType, resource.OperationCheckStatus) { - provisioner := registry.Get(request.ResourceType, resource.OperationCheckStatus, config.FromTargetConfig(request.TargetConfig)) + provisioner := registry.Get(request.ResourceType, resource.OperationCheckStatus, targetConfig) return provisioner.Status(ctx, request) } } - client, err := ccx.NewClient(config.FromTargetConfig(request.TargetConfig)) + client, err := ccx.NewClient(targetConfig) if err != nil { return nil, err } @@ -202,12 +223,13 @@ func (p *Plugin) Status(ctx context.Context, request *resource.StatusRequest) (* } func (p *Plugin) Delete(ctx context.Context, request *resource.DeleteRequest) (*resource.DeleteResult, error) { + targetConfig := config.FromTargetConfig(request.TargetConfig).WithOidcDeps(p.oidc) if registry.HasProvisioner(request.ResourceType, resource.OperationDelete) { - provisioner := registry.Get(request.ResourceType, resource.OperationDelete, config.FromTargetConfig(request.TargetConfig)) + provisioner := registry.Get(request.ResourceType, resource.OperationDelete, targetConfig) return provisioner.Delete(ctx, request) } - client, err := ccx.NewClient(config.FromTargetConfig(request.TargetConfig)) + client, err := ccx.NewClient(targetConfig) if err != nil { return nil, err } @@ -216,12 +238,13 @@ func (p *Plugin) Delete(ctx context.Context, request *resource.DeleteRequest) (* } func (p *Plugin) Read(ctx context.Context, request *resource.ReadRequest) (*resource.ReadResult, error) { + targetConfig := config.FromTargetConfig(request.TargetConfig).WithOidcDeps(p.oidc) if registry.HasProvisioner(request.ResourceType, resource.OperationRead) { - provisioner := registry.Get(request.ResourceType, resource.OperationRead, config.FromTargetConfig(request.TargetConfig)) + provisioner := registry.Get(request.ResourceType, resource.OperationRead, targetConfig) return provisioner.Read(ctx, request) } - client, err := ccx.NewClient(config.FromTargetConfig(request.TargetConfig)) + client, err := ccx.NewClient(targetConfig) if err != nil { return nil, err } @@ -230,12 +253,13 @@ func (p *Plugin) Read(ctx context.Context, request *resource.ReadRequest) (*reso } func (p *Plugin) List(ctx context.Context, request *resource.ListRequest) (*resource.ListResult, error) { + targetConfig := config.FromTargetConfig(request.TargetConfig).WithOidcDeps(p.oidc) if registry.HasProvisioner(request.ResourceType, resource.OperationList) { - provisioner := registry.Get(request.ResourceType, resource.OperationList, config.FromTargetConfig(request.TargetConfig)) + provisioner := registry.Get(request.ResourceType, resource.OperationList, targetConfig) return provisioner.List(ctx, request) } - client, err := ccx.NewClient(config.FromTargetConfig(request.TargetConfig)) + client, err := ccx.NewClient(targetConfig) if err != nil { return nil, err } diff --git a/aws_test.go b/aws_test.go index 2edfb673..526ca05d 100644 --- a/aws_test.go +++ b/aws_test.go @@ -7,11 +7,82 @@ package main import ( + "context" + "encoding/json" + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/platform-engineering-labs/formae/pkg/plugin" + "github.com/platform-engineering-labs/formae/pkg/plugin/resource" ) +// Compile-time check: Plugin must satisfy OidcAware, so the SDK can hand it +// an OidcTokenSource at startup. +var _ plugin.OidcAware = (*Plugin)(nil) + +// oidcTokenSourceFunc adapts a function to plugin.OidcTokenSource so a test +// can script what the source answers, and observe the ctx it was called +// with. +type oidcTokenSourceFunc func(ctx context.Context, audience string) (string, error) + +func (f oidcTokenSourceFunc) IdentityToken(ctx context.Context, audience string) (string, error) { + return f(ctx, audience) +} + +func TestSetOidcTokenSource_PopulatesDeps(t *testing.T) { + p := &Plugin{} + assert.Nil(t, p.oidc) + + src := oidcTokenSourceFunc(func(context.Context, string) (string, error) { + return "stub-token", nil + }) + p.SetOidcTokenSource(src) + + require.NotNil(t, p.oidc) + assert.NotNil(t, p.oidc.Source) +} + +// operationCtxMarkerKey is the context.WithValue marker used to prove a ctx +// travelled, unmodified in its values, from the operation call down into the +// OidcTokenSource. +type operationCtxMarkerKey struct{} + +// TestRead_OidcRoutesTheOperationCtxToTheTokenSource drives Plugin.Read (with +// a resource type carrying no registered provisioner, so it falls through to +// the CloudControl client) against a target config with an Oidc auth block. +// The stub token source returns an error, which fails the AWS SDK's +// credential resolution before it signs or sends any request, so the +// assertion never depends on network access or a real STS exchange; it only +// exercises Config->ToAwsConfig->CredentialsCache->Retrieve->IdentityToken +// far enough to observe the ctx that reached the token source. +func TestRead_OidcRoutesTheOperationCtxToTheTokenSource(t *testing.T) { + var gotCtx context.Context + src := oidcTokenSourceFunc(func(ctx context.Context, audience string) (string, error) { + gotCtx = ctx + assert.Equal(t, "sts.amazonaws.com", audience) + return "", errors.New("stub source: deliberately unminted, for ctx-routing assertions only") + }) + + p := &Plugin{} + p.SetOidcTokenSource(src) + + ctx := context.WithValue(context.Background(), operationCtxMarkerKey{}, "operation-ctx") + request := &resource.ReadRequest{ + ResourceType: "AWS::Formae::NoSuchProvisioneredType", + NativeID: "irrelevant-for-this-test", + TargetConfig: json.RawMessage(`{"Region":"us-east-1","Auth":{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"}}`), + } + + _, err := p.Read(ctx, request) + + require.Error(t, err) + require.NotNil(t, gotCtx) + assert.Equal(t, "operation-ctx", gotCtx.Value(operationCtxMarkerKey{})) +} + func TestMatchesFilter(t *testing.T) { t.Run("matches when all filter properties are present and equal", func(t *testing.T) { properties := `{"VpcId":"vpc-123","SubnetId":"subnet-456","CidrBlock":"10.0.0.0/24"}` diff --git a/go.sum b/go.sum index 1c01bebe..d7585392 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,12 @@ +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4 h1:JdoPbqQSv9iDCfkBFH5wDZCNpYHgJJVsrD7Xg0Jp57o= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4/go.mod h1:XbiEudzYCbg5YvbIMqylWXH2iWnUeKagDTfPR7uOaqg= github.com/JeroenSoeters/ergo v1.999.320-pel.6 h1:aq/v/tYTn+/QJZuMV6GDDnln2EUI+Wy3cVFhs7TB1lU= @@ -8,6 +15,10 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8 h1:6+qQvLkethJZQLb6r+Jxh0qxa7TfeFPBHlCzF7Ml8CM= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apple/pkl-go v0.13.2 h1:UCug0neH9Ufw0Loujosv5/UUXEXzY76HXbjqYqlPSGI= github.com/apple/pkl-go v0.13.2/go.mod h1:Ko3AgXOKd/vVYtsRZgoCZhymymz9RxqCIcfdZhOX85I= github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= @@ -28,6 +39,7 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 h1:5CrzwxDqf4w3x1Vs3 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36/go.mod h1:A3gHdKZIvG/QXERzZwcxNS3RNDFcRCuhhTFBYp+V/nw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 h1:A4N2f4YPcST0v+dWtX+xrpPPCL9VTBhoIFFUWYqbacE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36/go.mod h1:B/Qr859uxWUEfZeGotK5KAEoof4Q9YWgNtPSwV6jcyk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= github.com/aws/aws-sdk-go-v2/service/acm v1.39.4 h1:2P7p/kNLozilMJfF5SNfKCAslLZFtLmr7RjDHVni024= @@ -92,16 +104,23 @@ github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -110,10 +129,13 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= github.com/goforj/godump v1.9.1 h1:9OGpb978Ytz3B59d5Yi2PzRYYLid6UkmhYDIDNiF15Y= github.com/goforj/godump v1.9.1/go.mod h1:JsuL6AEZfKIU+iR5ewL6iQ2fIuhvLtPmJDH47M9Ptrc= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -124,6 +146,9 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -140,8 +165,10 @@ github.com/masterminds/semver v1.5.0 h1:hTxJTTY7tjvnWMrl08O6u3G6BLlKVwxSz01lVac9 github.com/masterminds/semver v1.5.0/go.mod h1:s7KNT9fnd7edGzwwP7RBX4H0v/CYd5qdOLfkL1V75yg= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df h1:1xWk/De6cs3h2rv2LZA/jcLFTmEGD9EluHsKaYq+Gm0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df/go.mod h1:sTJHuiEPB0WNdPYPOP9M6FQU4YTHemXgDLIQqkbBdDI= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 h1:ZMTgKwSomy2cVcl/+NivSqopbWeHbmYeQ+BxoYq8bVY= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1/go.mod h1:0ncHFCsGA6b0w1kBm6m+QwJ823qAY2vL47GvoR0BTyU= github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355 h1:em+eKL2p7m2L/LNbPThCzPefjtGcEPfnDM2YcQpGVIs= @@ -158,12 +185,16 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -190,14 +221,18 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246 h1:OB/t6xQE5QbY6m4bfg5f3kLCse6hL+X46HeQIG3F1lA= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246/go.mod h1:W9MwwilRxUgtLgXlceriUCNwM5j1lcMNAAq3Ya47C8Y= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/host v0.65.0 h1:cR4LpCn/2xDNdW3saBLrGJW7vWmrYlHYIhfuklhrlUc= go.opentelemetry.io/contrib/instrumentation/host v0.65.0/go.mod h1:laAqufqDgLYaaewUBpolv8GePmhIVqIeHyudbmi9KYk= go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ= @@ -218,10 +253,15 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -229,12 +269,16 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= +gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= @@ -252,3 +296,4 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027 h1:VSL2fw4r13Ve7yVD4RUlCfSlGmZF4AtJBv0fA4PhPvo= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/pkg/config/config.go b/pkg/config/config.go index 290cb227..cbbb4ec8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -46,6 +46,11 @@ var warnFlatFallback sync.Once // would outlive individual plugin instances and make tests order-dependent. // A Config with nil deps behaves flat-only: Oidc auth fails closed, and the // deprecation warning falls back to warnFlatFallback. +// +// Build one with NewOidcDeps, never a bare &OidcDeps{...} literal outside +// this package: a literal leaves stsFactory nil, and oidcCredentials calls it +// unconditionally once Source is non-nil, so a literal with a Source set +// panics on first use. NewOidcDeps always wires the production factory. type OidcDeps struct { // Source mints the OIDC identity tokens exchanged for AWS credentials. Source plugin.OidcTokenSource diff --git a/pkg/config/oidc_credentials.go b/pkg/config/oidc_credentials.go index 33c46ec5..5f2db494 100644 --- a/pkg/config/oidc_credentials.go +++ b/pkg/config/oidc_credentials.go @@ -100,6 +100,11 @@ func redactToken(err error, token string) error { // policy) yields a distinct cache rather than silently reusing credentials // minted under the old settings. The NUL separators keep a boundary shift // between the role and the region from colliding. +// +// The key deliberately omits the token source's identity: the SDK installs +// exactly one OidcTokenSource per plugin process (via OidcAware at startup), +// so within one OidcDeps every cache entry is already scoped to that single +// broker pairing and there is nothing for the key to distinguish. func oidcCacheKey(roleArn, region string, rawAuth json.RawMessage) string { sum := sha256.Sum256(rawAuth) From 4414b1c36dc292dd05ab9783c9fab7d1748b8f31 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 08:55:01 -0700 Subject: [PATCH 08/13] docs: document polymorphic auth and the release floor comment CHANGELOG: polymorphic auth (DefaultChainAuth | OidcAuth), the flat profile deprecation (works unchanged, warns once per process), the OidcAuth agent-broker requirement, and the one-time target-metadata update existing targets see on first reconcile after upgrade. formae-plugin.pkl: note beside minFormaeVersion that 0.89.0 must be the release that ships the oidc-credential broker, to be confirmed at release time rather than assumed from the current dev line. --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ formae-plugin.pkl | 4 ++++ 2 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1474c465..b2632216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,29 @@ Requires formae >= 0.89.0. a defect worth recording, or AWS-managed flooding without a filterable signal. +- Polymorphic `auth` on the target config: `DefaultChainAuth` (the existing + default credential provider chain — env vars, shared config, IMDS/IRSA — + optionally pinned to a shared-config `profile`) or `OidcAuth` (federated + identity: an OIDC identity token from the paired oidc-credential broker, + exchanged for credentials by assuming a role you name in the target + account). `auth` and the legacy flat `profile` field are mutually + exclusive; setting both is rejected at eval. + + `OidcAuth` requires a formae agent with oidc-credential broker support + (`minFormaeVersion = "0.89.0"` in `formae-plugin.pkl`). Paired with an + older agent, or an agent with no broker paired, credential resolution + fails closed with an explicit error rather than ever falling back to + ambient credentials. + + **One-time bookkeeping, not drift.** Existing targets carry no `auth` + block, and adding the field to the schema is a change formae records + against every target's stored metadata regardless of whether the target's + declared configuration actually changed. Expect a single, resource-inert + target-metadata update on the first reconcile after upgrading to this + version — no cloud resource is read, created, updated or destroyed by it. + If a stack shows exactly one such update per target immediately after the + upgrade, this is why; it is not drift and does not recur. + - `AWS::RDS::Database` and `AWS::RDS::DatabaseRole` support. A PostgreSQL database inside an Aurora cluster, and its owning login role, are now first-class declared resources. CloudControl models a cluster and its @@ -338,6 +361,16 @@ Requires formae >= 0.89.0. declaring the new five-resource forma. A later release will accept the older two-part identifier so this is unnecessary. +### Deprecated + +- The flat `profile` field on the target config is deprecated in favor of + `auth = new DefaultChainAuth { profile = ... }`. It continues to work + unchanged — a target set this way still authenticates via the default + credential chain pinned to that profile — and logs one deprecation warning + per plugin process rather than on every call. There is no removal in this + release; flat `profile` will be removed at a future major version, posted + ahead of time. + ### Fixed - An `AWS::CertificateManager::Certificate` with `subjectAlternativeNames` no diff --git a/formae-plugin.pkl b/formae-plugin.pkl index d61d2241..82e7d714 100644 --- a/formae-plugin.pkl +++ b/formae-plugin.pkl @@ -11,6 +11,10 @@ summary = "AWS resource plugin (CloudControl-based)" description = "AWS CloudControl resource plugin for Formae" category = "cloud" license = "FSL-1.1-ALv2" +// The dev line at planning time is 0.89.0-dev.2 (last stable 0.88.1), so +// 0.89.0 is the release expected to carry the oidc-credential broker this +// plugin's OidcAuth depends on. Confirm the real release number before +// publishing and correct this if that work lands in a later release instead. minFormaeVersion = "0.89.0" output { From 68d20d08a75a69b164ec2a4697f4da7b72ab9a83 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 09:09:43 -0700 Subject: [PATCH 09/13] fix(schema): hint the deprecated profile mutable Migrating a target from the flat profile to an auth block removes the top-level Profile key from the rendered config. Profile carried no config field hint, so the agent classified that as an immutable change and planned a target replace, which destroys and recreates every resource on the target. Hinting profile mutable, as auth already is, makes the deprecation's own migration an ordinary target update. --- schema/pkl/aws.pkl | 8 +++++++ schema_auth_test.go | 56 ++++++++++++++++++++++++++++++++------------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/schema/pkl/aws.pkl b/schema/pkl/aws.pkl index 07095848..f7f00a63 100644 --- a/schema/pkl/aws.pkl +++ b/schema/pkl/aws.pkl @@ -87,7 +87,15 @@ open class Config { hidden fixed type: String = "AWS" /// Deprecated: legacy spelling of the default credential chain. + /// + /// Mutable for the same reason `auth` is: the profile is an auth concern, + /// and how the agent authenticates is not where the resources live. + /// Marking it mutable makes this deprecation's own migration (flat + /// `profile` to an `auth` block) a target update rather than a target + /// replace, which would otherwise destroy and recreate every resource on + /// the target. @Deprecated { message = "set auth = new DefaultChainAuth { profile = ... } instead" } + @formae.ConfigFieldHint { createOnly = false } hidden profile: String? hidden region: Region diff --git a/schema_auth_test.go b/schema_auth_test.go index 4e854347..b8a8c65c 100644 --- a/schema_auth_test.go +++ b/schema_auth_test.go @@ -28,18 +28,28 @@ import ( "github.com/stretchr/testify/require" ) +// evalTarget is the slice of a rendered target this suite asserts on: the +// config the plugin will receive, and the config schema the agent classifies +// config changes against. +type evalTarget struct { + Config json.RawMessage `json:"Config"` + ConfigSchema struct { + Hints map[string]struct { + CreateOnly bool `json:"CreateOnly"` + } `json:"Hints"` + } `json:"ConfigSchema"` +} + type evalResult struct { - Targets []struct { - Config json.RawMessage `json:"Config"` - } `json:"Targets"` + Targets []evalTarget `json:"Targets"` } // runEval evaluates a testdata fixture with the formae binary named by -// FORMAE_BINARY and returns the target Config JSON of the first target. -// It fails the (sub)test if the binary is missing from FORMAE_BINARY, and -// returns the raw stderr and a non-nil error when evaluation itself fails -// so callers can assert on rejection. -func runEval(t *testing.T, binary, fixture string) (json.RawMessage, []byte, error) { +// FORMAE_BINARY and returns the first rendered target. It fails the (sub)test +// if the binary is missing from FORMAE_BINARY, and returns the raw stderr and +// a non-nil error when evaluation itself fails so callers can assert on +// rejection. +func runEval(t *testing.T, binary, fixture string) (evalTarget, []byte, error) { t.Helper() cmd := exec.Command(binary, "eval", //nolint:gosec // test-only, binary path comes from a trusted env var @@ -55,14 +65,14 @@ func runEval(t *testing.T, binary, fixture string) (json.RawMessage, []byte, err if exitErr, ok := err.(*exec.ExitError); ok { stderr = exitErr.Stderr } - return nil, stderr, err + return evalTarget{}, stderr, err } var result evalResult require.NoError(t, json.Unmarshal(stdout, &result), "eval output was not valid JSON: %s", stdout) require.Len(t, result.Targets, 1, "expected exactly one target in eval output") - return result.Targets[0].Config, nil, nil + return result.Targets[0], nil, nil } func TestAuthSchema(t *testing.T) { @@ -72,32 +82,46 @@ func TestAuthSchema(t *testing.T) { } t.Run("flat profile with no auth block renders Profile and omits Auth", func(t *testing.T) { - config, _, err := runEval(t, binary, "testdata/aws-config-auth-flat.pkl") + target, _, err := runEval(t, binary, "testdata/aws-config-auth-flat.pkl") require.NoError(t, err) - assert.JSONEq(t, `{"Type":"AWS","Profile":"legacy-profile","Region":"us-east-1"}`, string(config)) + assert.JSONEq(t, `{"Type":"AWS","Profile":"legacy-profile","Region":"us-east-1"}`, string(target.Config)) + }) + + t.Run("both auth spellings are hinted mutable so migrating between them updates the target", func(t *testing.T) { + target, _, err := runEval(t, binary, "testdata/aws-config-auth-flat.pkl") + require.NoError(t, err) + + profile, ok := target.ConfigSchema.Hints["Profile"] + require.True(t, ok, "Profile must carry a config field hint, or the agent classifies "+ + "dropping it during the auth migration as immutable and replaces the target") + assert.False(t, profile.CreateOnly) + + auth, ok := target.ConfigSchema.Hints["Auth"] + require.True(t, ok, "Auth must carry a config field hint") + assert.False(t, auth.CreateOnly) }) t.Run("auth = DefaultChainAuth renders a nested Auth object", func(t *testing.T) { - config, _, err := runEval(t, binary, "testdata/aws-config-auth-defaultchain.pkl") + target, _, err := runEval(t, binary, "testdata/aws-config-auth-defaultchain.pkl") require.NoError(t, err) assert.JSONEq(t, `{ "Type":"AWS", "Region":"us-east-1", "Auth":{"Type":"DefaultChain","Profile":"chain-profile"} - }`, string(config)) + }`, string(target.Config)) }) t.Run("auth = OidcAuth renders a nested Auth object", func(t *testing.T) { - config, _, err := runEval(t, binary, "testdata/aws-config-auth-oidc.pkl") + target, _, err := runEval(t, binary, "testdata/aws-config-auth-oidc.pkl") require.NoError(t, err) assert.JSONEq(t, `{ "Type":"AWS", "Region":"us-east-1", "Auth":{"Type":"Oidc","RoleArn":"arn:aws:iam::123456789012:role/formae-agent"} - }`, string(config)) + }`, string(target.Config)) }) t.Run("setting both profile and auth is rejected at eval", func(t *testing.T) { From 2a1b3812632b41da5e11eadd6b8ff919be1bb6fb Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 09:09:49 -0700 Subject: [PATCH 10/13] fix(config): name the web-identity session and default the STS factory Give the AssumeRoleWithWebIdentity exchange a fixed role session name so the assumed-role principal is attributable in the customer's CloudTrail instead of a random string. Read the STS client factory into a local and fall back to the production one when it is nil, so an OidcDeps built as a bare literal works without a write that would race a concurrent reader of the same struct. --- pkg/config/config.go | 18 +++++++++++------- pkg/config/oidc_credentials.go | 22 ++++++++++++++++++++-- pkg/config/oidc_credentials_test.go | 3 +++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index cbbb4ec8..163dadb0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -48,9 +48,9 @@ var warnFlatFallback sync.Once // deprecation warning falls back to warnFlatFallback. // // Build one with NewOidcDeps, never a bare &OidcDeps{...} literal outside -// this package: a literal leaves stsFactory nil, and oidcCredentials calls it -// unconditionally once Source is non-nil, so a literal with a Source set -// panics on first use. NewOidcDeps always wires the production factory. +// this package: NewOidcDeps wires the production STS factory. A literal +// leaves stsFactory nil and oidcCredentials falls back to the same factory, +// so it still works, but the seam is then invisible at the construction site. type OidcDeps struct { // Source mints the OIDC identity tokens exchanged for AWS credentials. Source plugin.OidcTokenSource @@ -68,14 +68,18 @@ type OidcDeps struct { warnFlat sync.Once } +// defaultSTSFactory is the production STS client factory: the client needs no +// credentials of its own, because AssumeRoleWithWebIdentity is unsigned. +func defaultSTSFactory(cfg aws.Config) stscreds.AssumeRoleWithWebIdentityAPIClient { + return sts.NewFromConfig(cfg) +} + // NewOidcDeps builds the OidcDeps a Plugin instance owns, wired to mint AWS // credentials against real STS. func NewOidcDeps(src plugin.OidcTokenSource) *OidcDeps { return &OidcDeps{ - Source: src, - stsFactory: func(cfg aws.Config) stscreds.AssumeRoleWithWebIdentityAPIClient { - return sts.NewFromConfig(cfg) - }, + Source: src, + stsFactory: defaultSTSFactory, } } diff --git a/pkg/config/oidc_credentials.go b/pkg/config/oidc_credentials.go index 5f2db494..88b12560 100644 --- a/pkg/config/oidc_credentials.go +++ b/pkg/config/oidc_credentials.go @@ -32,6 +32,12 @@ const oidcAudience = "sts.amazonaws.com" // indefinitely. const oidcRefreshTimeout = 30 * time.Second +// oidcRoleSessionName names every web-identity session this plugin opens. +// Without it the SDK generates a random name, so CloudTrail shows an +// unattributable assumed-role principal; a fixed name makes the caller +// legible in the customer's audit trail. +const oidcRoleSessionName = "formae-aws-plugin" + // oneShotRetriever hands the stock, context-free IdentityTokenRetriever a // token that was already minted for exactly one Retrieve call. Tokens are // short-lived and single-use by design, so a retriever is never reused. @@ -63,7 +69,10 @@ func (p *oidcCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials return aws.Credentials{}, fmt.Errorf("config: minting an identity token for role %q: %w", p.roleArn, err) } - exchange := stscreds.NewWebIdentityRoleProvider(p.stsClient, p.roleArn, oneShotRetriever{token: []byte(token)}) + exchange := stscreds.NewWebIdentityRoleProvider( + p.stsClient, p.roleArn, oneShotRetriever{token: []byte(token)}, + func(o *stscreds.WebIdentityRoleOptions) { o.RoleSessionName = oidcRoleSessionName }, + ) creds, err := exchange.Retrieve(refreshCtx) if err != nil { @@ -137,11 +146,20 @@ func (d *OidcDeps) credentialsCacheFor(key string, build func() *aws.Credentials // client is built from a region-only base config: it needs no credentials of // its own, because AssumeRoleWithWebIdentity is an unsigned call. func (d *OidcDeps) oidcCredentials(region, roleArn string, rawAuth json.RawMessage) *aws.CredentialsCache { + // Read the factory into a local rather than filling the field in: an + // OidcDeps built as a bare literal leaves it nil, and defaulting here + // keeps that recoverable without a write that would race a concurrent + // operation reading the same struct. + factory := d.stsFactory + if factory == nil { + factory = defaultSTSFactory + } + return d.credentialsCacheFor(oidcCacheKey(roleArn, region, rawAuth), func() *aws.CredentialsCache { return aws.NewCredentialsCache(&oidcCredentialsProvider{ source: d.Source, roleArn: roleArn, - stsClient: d.stsFactory(aws.Config{Region: region}), + stsClient: factory(aws.Config{Region: region}), }) }) } diff --git a/pkg/config/oidc_credentials_test.go b/pkg/config/oidc_credentials_test.go index 0f922c17..62717562 100644 --- a/pkg/config/oidc_credentials_test.go +++ b/pkg/config/oidc_credentials_test.go @@ -43,6 +43,7 @@ type fakeSTS struct { calls int token string roleArn string + sessionName string ctxErr error hadDeadline bool remaining time.Duration @@ -63,6 +64,7 @@ func (f *fakeSTS) AssumeRoleWithWebIdentity( f.calls++ f.token = aws.ToString(in.WebIdentityToken) f.roleArn = aws.ToString(in.RoleArn) + f.sessionName = aws.ToString(in.RoleSessionName) f.ctxErr = ctx.Err() if deadline, ok := ctx.Deadline(); ok { f.hadDeadline = true @@ -126,6 +128,7 @@ func TestRetrieve_ExchangesTheSourceTokenForCredentials(t *testing.T) { assert.Equal(t, 1, fake.callCount()) assert.Equal(t, "stub-token", fake.token) assert.Equal(t, testRoleArn, fake.roleArn) + assert.Equal(t, "formae-aws-plugin", fake.sessionName) assert.Equal(t, "AKIAEXAMPLE", creds.AccessKeyID) assert.Equal(t, "example-secret", creds.SecretAccessKey) assert.Equal(t, "example-session", creds.SessionToken) From 743c6c88ccf6fb346d886c920a3bfa15905795a2 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 09:09:49 -0700 Subject: [PATCH 11/13] docs(changelog): record the migration path and the STS exchange limits Say that moving a target from flat profile to an auth block is a normal update because both fields are mutable, and that an agent older than the release carrying those hints plans a target replace instead, so the migration has to follow the upgrade. Note that the OIDC token exchange runs on a default-configured STS client, so AWS_USE_FIPS_ENDPOINT, AWS_ENDPOINT_URL_STS and custom CA bundles are not honoured on it. --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2632216..eff54e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,15 @@ Requires formae >= 0.89.0. fails closed with an explicit error rather than ever falling back to ambient credentials. + **Known limitation: the STS exchange uses a default-configured client.** + The `AssumeRoleWithWebIdentity` call that turns the identity token into + credentials is made with a region-only STS client, so + `AWS_USE_FIPS_ENDPOINT`, `AWS_ENDPOINT_URL_STS` and a custom CA bundle are + not honoured on that one exchange. Proxy settings are honoured, because + they come from the HTTP transport rather than from SDK configuration. + Every other AWS call the plugin makes uses the fully configured client and + is unaffected. + **One-time bookkeeping, not drift.** Existing targets carry no `auth` block, and adding the field to the schema is a change formae records against every target's stored metadata regardless of whether the target's @@ -371,6 +380,17 @@ Requires formae >= 0.89.0. release; flat `profile` will be removed at a future major version, posted ahead of time. + **Migrating is a normal target update.** Both `profile` and `auth` are + declared mutable, so rewriting a target from the flat `profile` to an + `auth` block changes the target's configuration in place and touches no + cloud resource. + + This holds only on an agent at or above the release that carries these + hints. An **older** agent does not see them, classifies the dropped + top-level `Profile` key as an immutable change, and plans a target + replace, which destroys and recreates every resource on that target. + Upgrade the agent first, then migrate. + ### Fixed - An `AWS::CertificateManager::Certificate` with `subjectAlternativeNames` no From 1b6f3bec5e04b119b4a075beb557388ae123ce31 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 12:26:59 -0700 Subject: [PATCH 12/13] chore: track the typed identity-token wire --- go.mod | 4 ++-- go.sum | 53 ++++------------------------------------------------- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/go.mod b/go.mod index 3aad9ac1..9931acb6 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/platform-engineering-labs/formae/pkg/model v0.1.26 // SDK pinned at the credential-broker branch head. Re-pin to a real tag // before merge (release-path work). - github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821030446-6aa3db2765ec + github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821192428-ecbf54051536 github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630 github.com/stretchr/testify v1.11.1 ) @@ -78,7 +78,7 @@ require ( // pkg/plugin's own require for this is a zero-value placeholder (its // replace directive is repo-local and ignored by consumers), so it must // be pinned explicitly here. Re-pin to a real tag before merge (release-path work). - github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355 // indirect + github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821192428-ecbf54051536 // indirect github.com/platform-engineering-labs/orbital v0.1.36 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect diff --git a/go.sum b/go.sum index d7585392..92454595 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,5 @@ -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= -codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= -codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= -git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4 h1:JdoPbqQSv9iDCfkBFH5wDZCNpYHgJJVsrD7Xg0Jp57o= github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4/go.mod h1:XbiEudzYCbg5YvbIMqylWXH2iWnUeKagDTfPR7uOaqg= github.com/JeroenSoeters/ergo v1.999.320-pel.6 h1:aq/v/tYTn+/QJZuMV6GDDnln2EUI+Wy3cVFhs7TB1lU= @@ -15,10 +8,6 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8 h1:6+qQvLkethJZQLb6r+Jxh0qxa7TfeFPBHlCzF7Ml8CM= github.com/Sereal/Sereal/Go/sereal v0.0.0-20250307140414-035be09f1bc8/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apple/pkl-go v0.13.2 h1:UCug0neH9Ufw0Loujosv5/UUXEXzY76HXbjqYqlPSGI= github.com/apple/pkl-go v0.13.2/go.mod h1:Ko3AgXOKd/vVYtsRZgoCZhymymz9RxqCIcfdZhOX85I= github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= @@ -39,7 +28,6 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 h1:5CrzwxDqf4w3x1Vs3 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36/go.mod h1:A3gHdKZIvG/QXERzZwcxNS3RNDFcRCuhhTFBYp+V/nw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 h1:A4N2f4YPcST0v+dWtX+xrpPPCL9VTBhoIFFUWYqbacE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36/go.mod h1:B/Qr859uxWUEfZeGotK5KAEoof4Q9YWgNtPSwV6jcyk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= github.com/aws/aws-sdk-go-v2/service/acm v1.39.4 h1:2P7p/kNLozilMJfF5SNfKCAslLZFtLmr7RjDHVni024= @@ -104,23 +92,16 @@ github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -129,13 +110,10 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= github.com/goforj/godump v1.9.1 h1:9OGpb978Ytz3B59d5Yi2PzRYYLid6UkmhYDIDNiF15Y= github.com/goforj/godump v1.9.1/go.mod h1:JsuL6AEZfKIU+iR5ewL6iQ2fIuhvLtPmJDH47M9Ptrc= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -146,9 +124,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= -github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -165,18 +140,16 @@ github.com/masterminds/semver v1.5.0 h1:hTxJTTY7tjvnWMrl08O6u3G6BLlKVwxSz01lVac9 github.com/masterminds/semver v1.5.0/go.mod h1:s7KNT9fnd7edGzwwP7RBX4H0v/CYd5qdOLfkL1V75yg= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df h1:1xWk/De6cs3h2rv2LZA/jcLFTmEGD9EluHsKaYq+Gm0= github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df/go.mod h1:sTJHuiEPB0WNdPYPOP9M6FQU4YTHemXgDLIQqkbBdDI= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 h1:ZMTgKwSomy2cVcl/+NivSqopbWeHbmYeQ+BxoYq8bVY= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1/go.mod h1:0ncHFCsGA6b0w1kBm6m+QwJ823qAY2vL47GvoR0BTyU= -github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355 h1:em+eKL2p7m2L/LNbPThCzPefjtGcEPfnDM2YcQpGVIs= -github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821030437-d18cdc5fa355/go.mod h1:VS9hO24cYUcGYao9nfPb41HBfE2hF4WMrIS2fZwRwzM= +github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821192428-ecbf54051536 h1:qGMZwMrbmsyiuYOh2tQ3p0s1jECs5Ld4K09wbtvEjQo= +github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821192428-ecbf54051536/go.mod h1:VS9hO24cYUcGYao9nfPb41HBfE2hF4WMrIS2fZwRwzM= github.com/platform-engineering-labs/formae/pkg/model v0.1.26 h1:80p843bmz9sLTtUFveMGWYeGcpRjvceyVxUgPvlApqs= github.com/platform-engineering-labs/formae/pkg/model v0.1.26/go.mod h1:1dmsFwoaJZkHevBsAIZr068CWzG7de1VNz7PFWvM3Z8= -github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821030446-6aa3db2765ec h1:yrTUKa3bVNd3pr/tCcBx/onUdP39i7zFopuuzcVQC6M= -github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821030446-6aa3db2765ec/go.mod h1:2XJE4ZFchjdr+IDJz39PC9D41ziYe5lp1kAxpBWUD7w= +github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821192428-ecbf54051536 h1:wVEUYO/8GN2cPvZgJC8DrWqRL/Egec40eG4fWbvj+bs= +github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821192428-ecbf54051536/go.mod h1:2XJE4ZFchjdr+IDJz39PC9D41ziYe5lp1kAxpBWUD7w= github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630 h1:ZyGSai5xPIi+vxAS04JlVr04Fecw3K9Zj2bsR4OBkdE= github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630/go.mod h1:gq591ZeRZ4jAHpdSvCyrOvi8qyALxcXQQhd0B6GRMAo= github.com/platform-engineering-labs/orbital v0.1.36 h1:nPMLxDbwDrjlhJoMQXAE86HtVhQHC2A6BJlWYmNM75c= @@ -185,16 +158,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -221,18 +190,14 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246 h1:OB/t6xQE5QbY6m4bfg5f3kLCse6hL+X46HeQIG3F1lA= github.com/zps-io/sat v0.0.0-20190412034122-acaa8fa26246/go.mod h1:W9MwwilRxUgtLgXlceriUCNwM5j1lcMNAAq3Ya47C8Y= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/host v0.65.0 h1:cR4LpCn/2xDNdW3saBLrGJW7vWmrYlHYIhfuklhrlUc= go.opentelemetry.io/contrib/instrumentation/host v0.65.0/go.mod h1:laAqufqDgLYaaewUBpolv8GePmhIVqIeHyudbmi9KYk= go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ= @@ -253,15 +218,10 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -269,16 +229,12 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= -gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= @@ -296,4 +252,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027 h1:VSL2fw4r13Ve7yVD4RUlCfSlGmZF4AtJBv0fA4PhPvo= resty.dev/v3 v3.0.0-beta.6.0.20260127085140-f531c9de7027/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From aef65f4ce85de4559cd185ce1ab5ca5e9e8679bf Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 15:48:38 -0700 Subject: [PATCH 13/13] chore: pin the sdk and credential modules at formae main The previous pins pointed at a formae feature branch head, which is not reachable from main. Both now resolve from main: pkg/plugin at the commit that gives its own credential requirement a resolvable version, and pkg/credential at the commit on main that carries the module. The stale pre-merge notes are gone; the ergo fork replace stays, since a replace is not transitive and both modules need the -pel.6 revision. --- go.mod | 15 ++++++--------- go.sum | 8 ++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 9931acb6..5bd92949 100644 --- a/go.mod +++ b/go.mod @@ -31,9 +31,8 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/google/uuid v1.6.0 github.com/platform-engineering-labs/formae/pkg/model v0.1.26 - // SDK pinned at the credential-broker branch head. Re-pin to a real tag - // before merge (release-path work). - github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821192428-ecbf54051536 + // Re-pin to the next official pkg/plugin tag when one is cut. + github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821224650-dc5149d5a102 github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630 github.com/stretchr/testify v1.11.1 ) @@ -75,10 +74,8 @@ require ( github.com/miekg/dns v1.1.72 // indirect github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df // indirect github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 // indirect - // pkg/plugin's own require for this is a zero-value placeholder (its - // replace directive is repo-local and ignored by consumers), so it must - // be pinned explicitly here. Re-pin to a real tag before merge (release-path work). - github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821192428-ecbf54051536 // indirect + // Re-pin to the next official pkg/credential tag when one is cut. + github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821213704-ba68bacf6dd6 // indirect github.com/platform-engineering-labs/orbital v0.1.36 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect @@ -124,8 +121,8 @@ require ( ) // Matches the ergo fork revision required by the pinned pkg/plugin and -// pkg/credential (both need -pel.6). Re-pin to a real tag before merge -// (release-path work). +// pkg/credential (both need -pel.6). A replace directive is not transitive, +// so it has to be repeated here. replace ergo.services/ergo => github.com/JeroenSoeters/ergo v1.999.320-pel.6 replace ergo.services/actor/statemachine => github.com/JeroenSoeters/actor/statemachine v0.0.0-20260205190926-8b1b2eaf30f4 diff --git a/go.sum b/go.sum index 92454595..6b72a8ba 100644 --- a/go.sum +++ b/go.sum @@ -144,12 +144,12 @@ github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df h1:1xWk/De6cs3h2r github.com/naegelejd/go-acl v0.0.0-20260323030528-42e4d61407df/go.mod h1:sTJHuiEPB0WNdPYPOP9M6FQU4YTHemXgDLIQqkbBdDI= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 h1:ZMTgKwSomy2cVcl/+NivSqopbWeHbmYeQ+BxoYq8bVY= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1/go.mod h1:0ncHFCsGA6b0w1kBm6m+QwJ823qAY2vL47GvoR0BTyU= -github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821192428-ecbf54051536 h1:qGMZwMrbmsyiuYOh2tQ3p0s1jECs5Ld4K09wbtvEjQo= -github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821192428-ecbf54051536/go.mod h1:VS9hO24cYUcGYao9nfPb41HBfE2hF4WMrIS2fZwRwzM= +github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821213704-ba68bacf6dd6 h1:3F9pGvQDm4HHvPfVhohzjeCJZHvrVy3KCvPb1px7tLU= +github.com/platform-engineering-labs/formae/pkg/credential v0.0.0-20260821213704-ba68bacf6dd6/go.mod h1:VS9hO24cYUcGYao9nfPb41HBfE2hF4WMrIS2fZwRwzM= github.com/platform-engineering-labs/formae/pkg/model v0.1.26 h1:80p843bmz9sLTtUFveMGWYeGcpRjvceyVxUgPvlApqs= github.com/platform-engineering-labs/formae/pkg/model v0.1.26/go.mod h1:1dmsFwoaJZkHevBsAIZr068CWzG7de1VNz7PFWvM3Z8= -github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821192428-ecbf54051536 h1:wVEUYO/8GN2cPvZgJC8DrWqRL/Egec40eG4fWbvj+bs= -github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821192428-ecbf54051536/go.mod h1:2XJE4ZFchjdr+IDJz39PC9D41ziYe5lp1kAxpBWUD7w= +github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821224650-dc5149d5a102 h1:/heBYUEoIef2G50AmXZtXbDaDENoblspX2FFdy59zeo= +github.com/platform-engineering-labs/formae/pkg/plugin v0.4.2-0.20260821224650-dc5149d5a102/go.mod h1:VXdXrX5hBF3xXK0GIUowIhTBUaog0BmjU8TOXF2yUfA= github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630 h1:ZyGSai5xPIi+vxAS04JlVr04Fecw3K9Zj2bsR4OBkdE= github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.7-0.20260811042554-70c525251630/go.mod h1:gq591ZeRZ4jAHpdSvCyrOvi8qyALxcXQQhd0B6GRMAo= github.com/platform-engineering-labs/orbital v0.1.36 h1:nPMLxDbwDrjlhJoMQXAE86HtVhQHC2A6BJlWYmNM75c=