Stitch any CI into Tangled
77
fork

Configure Feed

Select the types of activity you want to include in your feed.

adding tekton first shot

authored by

Dylan Shepard and committed by
Tangled
2607c810 349dcd50

+1306 -2
+1
README.md
··· 110 110 its own doc per provider: 111 111 112 112 * [Buildkite](docs/buildkite.md) 113 + * [Tekton](docs/tekton.md)
+128
docs/tekton.md
··· 1 + # Tekton 2 + 3 + The Tekton provider runs only in Kubernetes. Tack receives Tangled 4 + pipeline triggers, creates a Tekton `PipelineRun` for an existing 5 + in-cluster `Pipeline`, watches that `PipelineRun`, and publishes 6 + `sh.tangled.pipeline.status` records back to Tangled. 7 + 8 + Tekton Triggers are intentionally not used. Tack already performs the 9 + event-to-run translation, and Tekton's native execution object is the 10 + `PipelineRun`. 11 + 12 + ## Required cluster setup 13 + 14 + * Tekton Pipelines is installed in the cluster. 15 + * Tack is deployed inside the same cluster. 16 + * The target Tekton `Pipeline` objects already exist in the namespace 17 + tack is configured to use. 18 + * Tack's Kubernetes service account has RBAC to: 19 + * create, get, list, and watch `tekton.dev` `pipelineruns` 20 + * get, list, and watch `tekton.dev` `taskruns` 21 + * get and list pods 22 + * get pod logs via `pods/log` 23 + 24 + Example RBAC: 25 + 26 + ```yaml 27 + apiVersion: rbac.authorization.k8s.io/v1 28 + kind: Role 29 + metadata: 30 + name: tack-tekton 31 + namespace: ci 32 + rules: 33 + - apiGroups: ["tekton.dev"] 34 + resources: ["pipelineruns"] 35 + verbs: ["create", "get", "list", "watch"] 36 + - apiGroups: ["tekton.dev"] 37 + resources: ["taskruns"] 38 + verbs: ["get", "list", "watch"] 39 + - apiGroups: [""] 40 + resources: ["pods"] 41 + verbs: ["get", "list"] 42 + - apiGroups: [""] 43 + resources: ["pods/log"] 44 + verbs: ["get"] 45 + ``` 46 + 47 + ## Configure Tack 48 + 49 + | Env var | Description | 50 + | ------------------------ | --------------------------------------------------------- | 51 + | `TACK_TEKTON_ENABLED` | Set to `1` to enable the Tekton provider | 52 + | `TACK_TEKTON_NAMESPACE` | Namespace for created `PipelineRun`s (default `default`) | 53 + 54 + The provider uses Kubernetes in-cluster service account credentials. 55 + It will not run from a local kubeconfig. 56 + 57 + ## Naming 58 + 59 + There are three separate names: 60 + 61 + * Tack workflow name: the Tangled workflow filename/name, e.g. `ci.yml`. 62 + This remains the Tangled-facing workflow identity in status records. 63 + * Tekton `Pipeline` name: the existing in-cluster pipeline definition, 64 + e.g. `repo-ci`. This is written to `spec.pipelineRef.name`. 65 + * Tekton `PipelineRun` name: generated by tack per trigger/workflow, 66 + e.g. `tack-ci-yml-<short-hash>`. This is the concrete execution 67 + object tack watches and stores. 68 + 69 + ## Workflow YAML 70 + 71 + Only the provider and target pipeline are required: 72 + 73 + ```yaml 74 + tack: 75 + tekton: 76 + pipeline: repo-ci 77 + ``` 78 + 79 + Optional fields: 80 + 81 + ```yaml 82 + tack: 83 + tekton: 84 + pipeline: repo-ci 85 + service_account: pipeline-runner 86 + params: 87 + image: example/app 88 + ``` 89 + 90 + `params` are forwarded as string Tekton params. Tack also stores the 91 + knot, pipeline rkey, workflow name, actor DID, commit, and branch as 92 + `PipelineRun` annotations, so operators can inspect the Kubernetes 93 + object and connect it back to the Tangled trigger. 94 + 95 + ## Example Pipeline 96 + 97 + ```yaml 98 + apiVersion: tekton.dev/v1 99 + kind: Pipeline 100 + metadata: 101 + name: repo-ci 102 + namespace: ci 103 + spec: 104 + params: 105 + - name: image 106 + type: string 107 + tasks: 108 + - name: test 109 + taskSpec: 110 + params: 111 + - name: image 112 + type: string 113 + steps: 114 + - name: test 115 + image: golang:1.25 116 + script: | 117 + set -eu 118 + echo "building $(params.image)" 119 + go test ./... 120 + workspaces: [] 121 + params: 122 + - name: image 123 + value: $(params.image) 124 + ``` 125 + 126 + Detailed CI behavior belongs in the in-cluster `Pipeline`. The Tangled 127 + workflow YAML should stay small: select `tekton`, pick the target 128 + pipeline, and pass only the small set of params that pipeline expects.
+28
go.mod
··· 8 8 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 9 9 github.com/mattn/go-sqlite3 v1.14.44 10 10 go.yaml.in/yaml/v2 v2.4.3 11 + k8s.io/api v0.35.3 12 + k8s.io/apimachinery v0.35.3 13 + k8s.io/client-go v0.35.3 11 14 tangled.org/core v1.13.0-alpha.0.20260502074102-37303f21368b 12 15 ) 13 16 ··· 29 32 github.com/charmbracelet/x/term v0.2.1 // indirect 30 33 github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d // indirect 31 34 github.com/cyphar/filepath-securejoin v0.4.1 // indirect 35 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 32 36 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 33 37 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 38 + github.com/emicklei/go-restful/v3 v3.12.2 // indirect 34 39 github.com/emirpasic/gods v1.18.1 // indirect 35 40 github.com/felixge/httpsnoop v1.0.4 // indirect 41 + github.com/fxamacker/cbor/v2 v2.9.0 // indirect 36 42 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 37 43 github.com/go-git/go-billy/v5 v5.6.2 // indirect 38 44 github.com/go-git/go-git/v5 v5.14.0 // indirect ··· 40 46 github.com/go-logfmt/logfmt v0.6.1 // indirect 41 47 github.com/go-logr/logr v1.4.3 // indirect 42 48 github.com/go-logr/stdr v1.2.2 // indirect 49 + github.com/go-openapi/jsonpointer v0.21.0 // indirect 50 + github.com/go-openapi/jsonreference v0.20.2 // indirect 51 + github.com/go-openapi/swag v0.23.0 // indirect 43 52 github.com/go-redis/cache/v9 v9.0.0 // indirect 44 53 github.com/goccy/go-json v0.10.5 // indirect 45 54 github.com/gogo/protobuf v1.3.2 // indirect 55 + github.com/google/gnostic-models v0.7.0 // indirect 46 56 github.com/google/uuid v1.6.0 // indirect 47 57 github.com/hashicorp/errwrap v1.1.0 // indirect 48 58 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect ··· 66 76 github.com/ipfs/go-log v1.0.5 // indirect 67 77 github.com/ipfs/go-log/v2 v2.9.1 // indirect 68 78 github.com/ipfs/go-metrics-interface v0.3.0 // indirect 79 + github.com/josharian/intern v1.0.0 // indirect 80 + github.com/json-iterator/go v1.1.12 // indirect 69 81 github.com/klauspost/compress v1.18.0 // indirect 70 82 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 71 83 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 84 + github.com/mailru/easyjson v0.7.7 // indirect 72 85 github.com/mattn/go-isatty v0.0.20 // indirect 73 86 github.com/mattn/go-runewidth v0.0.16 // indirect 74 87 github.com/minio/sha256-simd v1.0.1 // indirect 75 88 github.com/mitchellh/mapstructure v1.5.0 // indirect 89 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 90 + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 76 91 github.com/mr-tron/base58 v1.2.0 // indirect 77 92 github.com/muesli/termenv v0.16.0 // indirect 78 93 github.com/multiformats/go-base32 v0.1.0 // indirect ··· 98 113 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 99 114 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 100 115 github.com/whyrusleeping/cbor-gen v0.3.1 // indirect 116 + github.com/x448/float16 v0.8.4 // indirect 101 117 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 102 118 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 103 119 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect ··· 109 125 go.uber.org/atomic v1.11.0 // indirect 110 126 go.uber.org/multierr v1.11.0 // indirect 111 127 go.uber.org/zap v1.27.1 // indirect 128 + go.yaml.in/yaml/v3 v3.0.4 // indirect 112 129 golang.org/x/crypto v0.48.0 // indirect 113 130 golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect 114 131 golang.org/x/net v0.50.0 // indirect 132 + golang.org/x/oauth2 v0.34.0 // indirect 115 133 golang.org/x/sync v0.19.0 // indirect 116 134 golang.org/x/sys v0.42.0 // indirect 135 + golang.org/x/term v0.40.0 // indirect 117 136 golang.org/x/text v0.34.0 // indirect 118 137 golang.org/x/time v0.12.0 // indirect 119 138 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 120 139 google.golang.org/protobuf v1.36.11 // indirect 140 + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect 141 + gopkg.in/inf.v0 v0.9.1 // indirect 121 142 gopkg.in/warnings.v0 v0.1.2 // indirect 122 143 gopkg.in/yaml.v3 v3.0.1 // indirect 144 + k8s.io/klog/v2 v2.130.1 // indirect 145 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect 146 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect 123 147 lukechampine.com/blake3 v1.4.1 // indirect 148 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 149 + sigs.k8s.io/randfill v1.0.0 // indirect 150 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 151 + sigs.k8s.io/yaml v1.6.0 // indirect 124 152 ) 125 153 126 154 // tangled.org/core uses these forks for extra commit/patch metadata fields.
+79 -2
go.sum
··· 1 1 dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 2 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 3 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 + github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 5 + github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 4 6 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 5 7 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 6 8 github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= ··· 62 64 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 63 65 github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 64 66 github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 67 + github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 68 + github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 65 69 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 66 70 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 67 71 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= ··· 72 76 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 73 77 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 74 78 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 79 + github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 80 + github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 75 81 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 76 82 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 77 83 github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= ··· 88 94 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 89 95 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 90 96 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 97 + github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 98 + github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 99 + github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 100 + github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 101 + github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 102 + github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 103 + github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 104 + github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 91 105 github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0= 92 106 github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI= 107 + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 93 108 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 109 + github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 110 + github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 94 111 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 95 112 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 96 113 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= ··· 109 126 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 110 127 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 111 128 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 129 + github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 130 + github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 112 131 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 113 132 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 114 133 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= ··· 117 136 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 118 137 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 119 138 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 139 + github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 120 140 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 141 + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 142 + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 121 143 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 122 144 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 123 145 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= ··· 182 204 github.com/ipfs/go-log/v2 v2.9.1/go.mod h1:evFx7sBiohUN3AG12mXlZBw5hacBQld3ZPHrowlJYoo= 183 205 github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 184 206 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 207 + github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 208 + github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 209 + github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 210 + github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 185 211 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 186 212 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 187 213 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= ··· 194 220 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 195 221 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 196 222 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 223 + github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 197 224 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 198 225 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 199 226 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= ··· 203 230 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 204 231 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 205 232 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 233 + github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 234 + github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 206 235 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 207 236 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 208 237 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= ··· 215 244 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 216 245 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 217 246 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 247 + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 248 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 249 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 250 + github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 251 + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 252 + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 218 253 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 219 254 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 220 255 github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= ··· 246 281 github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 247 282 github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= 248 283 github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 284 + github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= 285 + github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= 249 286 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 250 287 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 251 288 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= ··· 256 293 github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 257 294 github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 258 295 github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 259 - github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 260 - github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 296 + github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 297 + github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 261 298 github.com/openbao/openbao/api/v2 v2.3.0 h1:61FO3ILtpKoxbD9kTWeGaCq8pz1sdt4dv2cmTXsiaAc= 262 299 github.com/openbao/openbao/api/v2 v2.3.0/go.mod h1:T47WKHb7DqHa3Ms3xicQtl5EiPE+U8diKjb9888okWs= 263 300 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= ··· 305 342 github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 306 343 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 307 344 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 345 + github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 346 + github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 308 347 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 309 348 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 310 349 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 350 + github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 351 + github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 311 352 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 312 353 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 313 354 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= ··· 330 371 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 331 372 github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 332 373 github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 374 + github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 375 + github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 333 376 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 334 377 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 335 378 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= ··· 370 413 go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 371 414 go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 372 415 go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 416 + go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 417 + go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 373 418 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 374 419 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 375 420 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= ··· 390 435 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 391 436 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 392 437 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 438 + golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= 439 + golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 393 440 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 394 441 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 395 442 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= ··· 411 458 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 412 459 golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= 413 460 golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= 461 + golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= 462 + golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 414 463 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 464 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 416 465 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 458 507 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 459 508 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 460 509 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 510 + golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= 511 + golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= 461 512 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 462 513 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 463 514 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ··· 487 538 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 488 539 golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 489 540 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 541 + golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= 542 + golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= 490 543 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 491 544 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 492 545 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= ··· 510 563 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 511 564 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 512 565 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 566 + gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 567 + gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 513 568 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 569 + gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 570 + gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 514 571 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 515 572 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 516 573 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= ··· 523 580 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 524 581 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 525 582 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 583 + k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= 584 + k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= 585 + k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= 586 + k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= 587 + k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= 588 + k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= 589 + k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 590 + k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 591 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= 592 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 593 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= 594 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 526 595 lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= 527 596 lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= 597 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 598 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 599 + sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 600 + sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 601 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 602 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 603 + sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 604 + sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 528 605 tangled.org/core v1.13.0-alpha.0.20260502074102-37303f21368b h1:5g3kGPrs6IGoHuFAb3kqrL5EwgGTl4ulbJTYsX1WfTM= 529 606 tangled.org/core v1.13.0-alpha.0.20260502074102-37303f21368b/go.mod h1:abXVlFoPAeM9pZier/WF1Cnn1ZoO9YE5h59WLEUy+Hk= 530 607 tangled.sh/oppi.li/go-gitdiff v0.8.2 h1:pASJJNWaFn6EmEIUNNjHZQ3stRu6BqTO2YyjKvTcxIc=
+26
main.go
··· 46 46 BuildkiteOrg string 47 47 BuildkiteWebhookSecret string 48 48 BuildkiteWebhookMode buildkite.WebhookMode 49 + 50 + // Tekton mode is explicit because it only works from inside a 51 + // Kubernetes cluster. When enabled, tack creates PipelineRuns in 52 + // TektonNamespace using its pod's service account credentials. 53 + TektonEnabled bool 54 + TektonNamespace string 49 55 } 50 56 51 57 func loadConfig() (config, error) { ··· 62 68 BuildkiteWebhookMode: buildkite.WebhookMode( 63 69 envOr("TACK_BUILDKITE_WEBHOOK_MODE", string(buildkite.WebhookModeToken)), 64 70 ), 71 + TektonEnabled: os.Getenv("TACK_TEKTON_ENABLED") == "1", 72 + TektonNamespace: envOr("TACK_TEKTON_NAMESPACE", "default"), 65 73 } 66 74 addrFlag := flag.String("addr", cfg.Addr, "HTTP listen address (overrides TACK_LISTEN_ADDR)") 67 75 flag.Parse() ··· 99 107 cfg.BuildkiteWebhookMode, 100 108 ) 101 109 } 110 + } 111 + if cfg.TektonEnabled && cfg.TektonNamespace == "" { 112 + return cfg, errors.New("TACK_TEKTON_NAMESPACE is required when TACK_TEKTON_ENABLED=1") 102 113 } 103 114 104 115 return cfg, nil ··· 189 200 logger.Info("buildkite provider enabled", 190 201 "default_org", cfg.BuildkiteOrg, 191 202 "webhook_mode", cfg.BuildkiteWebhookMode, 203 + ) 204 + } 205 + if cfg.TektonEnabled { 206 + tkProvider, err := newInClusterTektonProvider( 207 + br, st, 208 + cfg.TektonNamespace, 209 + logger, 210 + ) 211 + if err != nil { 212 + logger.Error("failed to configure tekton provider", "err", err) 213 + os.Exit(1) 214 + } 215 + providers["tekton"] = tkProvider 216 + logger.Info("tekton provider enabled", 217 + "namespace", cfg.TektonNamespace, 192 218 ) 193 219 } 194 220 provider := newProviderRouter(logger, providers)
+663
provider_tekton.go
··· 1 + package main 2 + 3 + // tektonProvider implements Provider by creating Tekton PipelineRuns 4 + // directly inside the Kubernetes cluster. Tack already receives and 5 + // authorizes Tangled pipeline triggers, so adding Tekton Triggers would 6 + // duplicate the event-to-run translation layer instead of simplifying it. 7 + 8 + import ( 9 + "bufio" 10 + "context" 11 + "crypto/sha256" 12 + "encoding/hex" 13 + "encoding/json" 14 + "errors" 15 + "fmt" 16 + "io" 17 + "log/slog" 18 + "sort" 19 + "strings" 20 + "time" 21 + "unicode" 22 + 23 + corev1 "k8s.io/api/core/v1" 24 + apierrors "k8s.io/apimachinery/pkg/api/errors" 25 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 + "k8s.io/apimachinery/pkg/fields" 28 + "k8s.io/apimachinery/pkg/labels" 29 + runtimeschema "k8s.io/apimachinery/pkg/runtime/schema" 30 + "k8s.io/client-go/dynamic" 31 + "k8s.io/client-go/kubernetes" 32 + "k8s.io/client-go/rest" 33 + "tangled.org/core/api/tangled" 34 + 35 + "go.yaml.in/yaml/v2" 36 + ) 37 + 38 + const ( 39 + tektonAPIVersion = "tekton.dev/v1" 40 + tektonRunKind = "PipelineRun" 41 + 42 + tektonLabelManagedBy = "tack.mitchellh.com/managed-by" 43 + tektonLabelPipelineRkey = "tack.mitchellh.com/pipeline-rkey" 44 + tektonLabelWorkflow = "tack.mitchellh.com/workflow" 45 + 46 + tektonAnnotationKnot = "tack.mitchellh.com/knot" 47 + tektonAnnotationPipelineRkey = "tack.mitchellh.com/pipeline-rkey" 48 + tektonAnnotationWorkflow = "tack.mitchellh.com/workflow" 49 + tektonAnnotationActor = "tack.mitchellh.com/actor" 50 + tektonAnnotationCommit = "tack.mitchellh.com/commit" 51 + tektonAnnotationBranch = "tack.mitchellh.com/branch" 52 + ) 53 + 54 + var ( 55 + pipelineRunsGVR = runtimeschema.GroupVersionResource{ 56 + Group: "tekton.dev", Version: "v1", Resource: "pipelineruns", 57 + } 58 + taskRunsGVR = runtimeschema.GroupVersionResource{ 59 + Group: "tekton.dev", Version: "v1", Resource: "taskruns", 60 + } 61 + ) 62 + 63 + // tektonWorkflowConfig is the Tekton-specific subset of workflow YAML. 64 + // `pipeline` names an existing in-cluster Tekton Pipeline. Params are 65 + // deliberately string-only in v1: tack is meant to select an existing 66 + // runner and pass a small amount of routing data, not mirror Tekton's 67 + // entire PipelineRun API. 68 + type tektonWorkflowConfig struct { 69 + Pipeline string `yaml:"pipeline"` 70 + ServiceAccount string `yaml:"service_account"` 71 + Params map[string]string `yaml:"params"` 72 + } 73 + 74 + type tektonWorkflowDoc struct { 75 + Tack struct { 76 + Tekton tektonWorkflowConfig `yaml:"tekton"` 77 + } `yaml:"tack"` 78 + } 79 + 80 + // parseTektonWorkflowConfig decodes `tack.tekton` from a workflow body. 81 + func parseTektonWorkflowConfig(raw string) (*tektonWorkflowConfig, error) { 82 + if strings.TrimSpace(raw) == "" { 83 + return nil, errors.New("workflow body is empty") 84 + } 85 + var doc tektonWorkflowDoc 86 + if err := yaml.Unmarshal([]byte(raw), &doc); err != nil { 87 + return nil, fmt.Errorf("parse workflow yaml: %w", err) 88 + } 89 + cfg := doc.Tack.Tekton 90 + if cfg.Pipeline == "" { 91 + return nil, errors.New("workflow yaml: `tack.tekton.pipeline` is required") 92 + } 93 + return &cfg, nil 94 + } 95 + 96 + type tektonProvider struct { 97 + br *broker 98 + st *store 99 + log *slog.Logger 100 + dyn dynamic.Interface 101 + kube kubernetes.Interface 102 + namespace string 103 + } 104 + 105 + var _ Provider = (*tektonProvider)(nil) 106 + 107 + func newTektonProvider( 108 + br *broker, 109 + st *store, 110 + dyn dynamic.Interface, 111 + kube kubernetes.Interface, 112 + namespace string, 113 + log *slog.Logger, 114 + ) *tektonProvider { 115 + return &tektonProvider{ 116 + br: br, 117 + st: st, 118 + log: log.With("component", "provider", "kind", "tekton"), 119 + dyn: dyn, 120 + kube: kube, 121 + namespace: namespace, 122 + } 123 + } 124 + 125 + func newInClusterTektonProvider( 126 + br *broker, 127 + st *store, 128 + namespace string, 129 + log *slog.Logger, 130 + ) (*tektonProvider, error) { 131 + cfg, err := rest.InClusterConfig() 132 + if err != nil { 133 + return nil, fmt.Errorf("load in-cluster kubernetes config: %w", err) 134 + } 135 + dyn, err := dynamic.NewForConfig(cfg) 136 + if err != nil { 137 + return nil, fmt.Errorf("create dynamic kubernetes client: %w", err) 138 + } 139 + kube, err := kubernetes.NewForConfig(cfg) 140 + if err != nil { 141 + return nil, fmt.Errorf("create kubernetes client: %w", err) 142 + } 143 + return newTektonProvider(br, st, dyn, kube, namespace, log), nil 144 + } 145 + 146 + func (p *tektonProvider) Spawn( 147 + ctx context.Context, 148 + knot string, 149 + pipelineRkey string, 150 + actor string, 151 + trigger *tangled.Pipeline_TriggerMetadata, 152 + workflows []*tangled.Pipeline_Workflow, 153 + ) { 154 + if len(workflows) == 0 { 155 + p.log.Warn("pipeline has no workflows; nothing to spawn", 156 + "knot", knot, "rkey", pipelineRkey, 157 + ) 158 + return 159 + } 160 + for _, wf := range workflows { 161 + if wf == nil || wf.Name == "" { 162 + continue 163 + } 164 + wf := wf 165 + go p.spawnWorkflow(ctx, knot, pipelineRkey, actor, trigger, wf) 166 + } 167 + } 168 + 169 + func (p *tektonProvider) spawnWorkflow( 170 + ctx context.Context, 171 + knot string, 172 + pipelineRkey string, 173 + actor string, 174 + trigger *tangled.Pipeline_TriggerMetadata, 175 + wf *tangled.Pipeline_Workflow, 176 + ) { 177 + logger := p.log.With( 178 + "knot", knot, 179 + "pipeline_rkey", pipelineRkey, 180 + "workflow", wf.Name, 181 + "actor", actor, 182 + ) 183 + 184 + cfg, err := parseTektonWorkflowConfig(wf.Raw) 185 + if err != nil { 186 + logger.Error("invalid workflow config; refusing to spawn", "err", err) 187 + return 188 + } 189 + commit, branch := triggerCommitAndBranch(trigger) 190 + name := tektonPipelineRunName(knot, pipelineRkey, wf.Name, commit, branch) 191 + pr := buildTektonPipelineRun( 192 + p.namespace, name, cfg, knot, pipelineRkey, actor, commit, branch, wf, 193 + ) 194 + 195 + runs := p.dyn.Resource(pipelineRunsGVR).Namespace(p.namespace) 196 + created, err := runs.Create(ctx, pr, metav1.CreateOptions{}) 197 + if apierrors.IsAlreadyExists(err) { 198 + created, err = runs.Get(ctx, name, metav1.GetOptions{}) 199 + } 200 + if err != nil { 201 + logger.Error("create tekton PipelineRun", "err", err, 202 + "namespace", p.namespace, "pipeline_run", name, 203 + "pipeline", cfg.Pipeline, 204 + ) 205 + return 206 + } 207 + 208 + ref := TektonRunRef{ 209 + Knot: knot, 210 + PipelineRkey: pipelineRkey, 211 + Workflow: wf.Name, 212 + Namespace: p.namespace, 213 + PipelineRunName: name, 214 + PipelineRunUID: string(created.GetUID()), 215 + PipelineName: cfg.Pipeline, 216 + PipelineURI: pipelineATURI(knot, pipelineRkey), 217 + } 218 + if err := p.st.InsertTektonRun(ctx, ref); err != nil { 219 + logger.Error("persist tekton run mapping", "err", err, 220 + "pipeline_run", name, 221 + ) 222 + return 223 + } 224 + 225 + if err := p.publishStatus(ctx, ref.PipelineURI, wf.Name, 226 + "pending", name, nil, nil); err != nil { 227 + logger.Error("publish initial pending status", "err", err) 228 + } 229 + 230 + logger.Info("tekton PipelineRun created", 231 + "namespace", p.namespace, 232 + "pipeline", cfg.Pipeline, 233 + "pipeline_run", name, 234 + "uid", ref.PipelineRunUID, 235 + ) 236 + go p.watchPipelineRun(ctx, ref) 237 + } 238 + 239 + func buildTektonPipelineRun( 240 + namespace, name string, 241 + cfg *tektonWorkflowConfig, 242 + knot, pipelineRkey, actor, commit, branch string, 243 + wf *tangled.Pipeline_Workflow, 244 + ) *unstructured.Unstructured { 245 + obj := &unstructured.Unstructured{ 246 + Object: map[string]interface{}{ 247 + "apiVersion": tektonAPIVersion, 248 + "kind": tektonRunKind, 249 + "metadata": map[string]interface{}{ 250 + "name": name, 251 + "namespace": namespace, 252 + "labels": map[string]interface{}{ 253 + tektonLabelManagedBy: "tack", 254 + tektonLabelPipelineRkey: labelValue(pipelineRkey), 255 + tektonLabelWorkflow: labelValue(wf.Name), 256 + }, 257 + "annotations": map[string]interface{}{ 258 + tektonAnnotationKnot: knot, 259 + tektonAnnotationPipelineRkey: pipelineRkey, 260 + tektonAnnotationWorkflow: wf.Name, 261 + tektonAnnotationActor: actor, 262 + tektonAnnotationCommit: commit, 263 + tektonAnnotationBranch: branch, 264 + }, 265 + }, 266 + "spec": map[string]interface{}{ 267 + "pipelineRef": map[string]interface{}{ 268 + "name": cfg.Pipeline, 269 + }, 270 + }, 271 + }, 272 + } 273 + spec := obj.Object["spec"].(map[string]interface{}) 274 + if cfg.ServiceAccount != "" { 275 + spec["serviceAccountName"] = cfg.ServiceAccount 276 + } 277 + if len(cfg.Params) > 0 { 278 + keys := make([]string, 0, len(cfg.Params)) 279 + for key := range cfg.Params { 280 + keys = append(keys, key) 281 + } 282 + sort.Strings(keys) 283 + params := make([]interface{}, 0, len(keys)) 284 + for _, key := range keys { 285 + params = append(params, map[string]interface{}{ 286 + "name": key, 287 + "value": cfg.Params[key], 288 + }) 289 + } 290 + spec["params"] = params 291 + } 292 + return obj 293 + } 294 + 295 + func (p *tektonProvider) watchPipelineRun(ctx context.Context, ref TektonRunRef) { 296 + logger := p.log.With( 297 + "knot", ref.Knot, 298 + "pipeline_rkey", ref.PipelineRkey, 299 + "workflow", ref.Workflow, 300 + "namespace", ref.Namespace, 301 + "pipeline_run", ref.PipelineRunName, 302 + ) 303 + 304 + last := "" 305 + if obj, err := p.dyn.Resource(pipelineRunsGVR).Namespace(ref.Namespace). 306 + Get(ctx, ref.PipelineRunName, metav1.GetOptions{}); err == nil { 307 + status, terminal, ok := mapTektonPipelineRunStatus(obj) 308 + if ok { 309 + last = status 310 + if err := p.publishStatus(ctx, ref.PipelineURI, ref.Workflow, 311 + status, ref.PipelineRunName, nil, nil); err != nil { 312 + logger.Error("publish tekton status", "err", err, "status", status) 313 + } 314 + if terminal { 315 + return 316 + } 317 + } 318 + } else if apierrors.IsNotFound(err) { 319 + logger.Warn("PipelineRun disappeared while watching") 320 + return 321 + } else { 322 + logger.Debug("initial PipelineRun status read", "err", err) 323 + } 324 + 325 + w, err := p.dyn.Resource(pipelineRunsGVR).Namespace(ref.Namespace). 326 + Watch(ctx, metav1.ListOptions{ 327 + FieldSelector: fields.OneTermEqualSelector( 328 + "metadata.name", ref.PipelineRunName, 329 + ).String(), 330 + }) 331 + if err != nil { 332 + logger.Debug("watch PipelineRun status; falling back to polling", "err", err) 333 + p.pollPipelineRun(ctx, ref, logger, last) 334 + return 335 + } 336 + defer w.Stop() 337 + 338 + for { 339 + select { 340 + case <-ctx.Done(): 341 + return 342 + case ev, ok := <-w.ResultChan(): 343 + if !ok { 344 + p.pollPipelineRun(ctx, ref, logger, last) 345 + return 346 + } 347 + obj, ok := ev.Object.(*unstructured.Unstructured) 348 + if !ok { 349 + continue 350 + } 351 + status, terminal, ok := mapTektonPipelineRunStatus(obj) 352 + if !ok || status == last { 353 + if terminal { 354 + return 355 + } 356 + continue 357 + } 358 + last = status 359 + if err := p.publishStatus(ctx, ref.PipelineURI, ref.Workflow, 360 + status, ref.PipelineRunName, nil, nil); err != nil { 361 + logger.Error("publish tekton status", "err", err, "status", status) 362 + continue 363 + } 364 + if terminal { 365 + return 366 + } 367 + } 368 + } 369 + } 370 + 371 + func (p *tektonProvider) pollPipelineRun( 372 + ctx context.Context, 373 + ref TektonRunRef, 374 + logger *slog.Logger, 375 + last string, 376 + ) { 377 + ticker := time.NewTicker(5 * time.Second) 378 + defer ticker.Stop() 379 + for { 380 + select { 381 + case <-ctx.Done(): 382 + return 383 + case <-ticker.C: 384 + obj, err := p.dyn.Resource(pipelineRunsGVR).Namespace(ref.Namespace). 385 + Get(ctx, ref.PipelineRunName, metav1.GetOptions{}) 386 + if apierrors.IsNotFound(err) { 387 + logger.Warn("PipelineRun disappeared while watching") 388 + return 389 + } 390 + if err != nil { 391 + logger.Debug("get PipelineRun status", "err", err) 392 + continue 393 + } 394 + status, terminal, ok := mapTektonPipelineRunStatus(obj) 395 + if !ok || status == last { 396 + if terminal { 397 + return 398 + } 399 + continue 400 + } 401 + last = status 402 + if err := p.publishStatus(ctx, ref.PipelineURI, ref.Workflow, 403 + status, ref.PipelineRunName, nil, nil); err != nil { 404 + logger.Error("publish tekton status", "err", err, "status", status) 405 + continue 406 + } 407 + if terminal { 408 + return 409 + } 410 + } 411 + } 412 + } 413 + 414 + // mapTektonPipelineRunStatus translates Tekton's Succeeded condition 415 + // into the Tangled status strings consumed by the appview. 416 + func mapTektonPipelineRunStatus(obj *unstructured.Unstructured) (status string, terminal bool, ok bool) { 417 + conditions, ok, _ := unstructured.NestedSlice(obj.Object, "status", "conditions") 418 + if !ok || len(conditions) == 0 { 419 + return "", false, false 420 + } 421 + for _, raw := range conditions { 422 + cond, _ := raw.(map[string]interface{}) 423 + if cond["type"] != "Succeeded" { 424 + continue 425 + } 426 + condStatus, _ := cond["status"].(string) 427 + reason, _ := cond["reason"].(string) 428 + switch condStatus { 429 + case "True": 430 + return "success", true, true 431 + case "False": 432 + if tektonReasonCancelled(reason) { 433 + return "cancelled", true, true 434 + } 435 + return "failed", true, true 436 + case "Unknown": 437 + return "running", false, true 438 + default: 439 + return "", false, false 440 + } 441 + } 442 + return "", false, false 443 + } 444 + 445 + func tektonReasonCancelled(reason string) bool { 446 + r := strings.ToLower(reason) 447 + return strings.Contains(r, "cancel") || strings.Contains(r, "stop") 448 + } 449 + 450 + func (p *tektonProvider) Logs( 451 + ctx context.Context, 452 + knot string, 453 + pipelineRkey string, 454 + workflow string, 455 + ) (<-chan LogLine, error) { 456 + ref, err := p.st.LookupTektonRunByTuple(ctx, knot, pipelineRkey, workflow) 457 + if err != nil { 458 + return nil, fmt.Errorf("lookup tekton run mapping: %w", err) 459 + } 460 + if ref == nil { 461 + return nil, ErrLogsNotFound 462 + } 463 + 464 + taskRuns, err := p.taskRunsForPipelineRun(ctx, *ref) 465 + if err != nil { 466 + return nil, err 467 + } 468 + if len(taskRuns) == 0 { 469 + return nil, ErrLogsNotFound 470 + } 471 + 472 + out := make(chan LogLine, 32) 473 + go func() { 474 + defer close(out) 475 + stepID := 0 476 + for _, tr := range taskRuns { 477 + taskName := tr.GetName() 478 + if taskName == "" { 479 + taskName = fmt.Sprintf("task %d", stepID) 480 + } 481 + if !sendLine(ctx, out, LogLine{ 482 + Kind: LogKindControl, 483 + Time: time.Now(), 484 + Content: taskName, 485 + StepId: stepID, 486 + StepStatus: StepStatusStart, 487 + }) { 488 + return 489 + } 490 + 491 + p.streamTaskRunLogs(ctx, out, *ref, tr, stepID) 492 + 493 + if !sendLine(ctx, out, LogLine{ 494 + Kind: LogKindControl, 495 + Time: time.Now(), 496 + Content: taskName, 497 + StepId: stepID, 498 + StepStatus: StepStatusEnd, 499 + }) { 500 + return 501 + } 502 + stepID++ 503 + } 504 + }() 505 + return out, nil 506 + } 507 + 508 + func (p *tektonProvider) taskRunsForPipelineRun(ctx context.Context, ref TektonRunRef) ([]unstructured.Unstructured, error) { 509 + sel := labels.Set{"tekton.dev/pipelineRun": ref.PipelineRunName}.String() 510 + list, err := p.dyn.Resource(taskRunsGVR).Namespace(ref.Namespace). 511 + List(ctx, metav1.ListOptions{LabelSelector: sel}) 512 + if err != nil { 513 + return nil, fmt.Errorf("list Tekton TaskRuns: %w", err) 514 + } 515 + items := append([]unstructured.Unstructured(nil), list.Items...) 516 + sort.Slice(items, func(i, j int) bool { 517 + ti := items[i].GetCreationTimestamp() 518 + tj := items[j].GetCreationTimestamp() 519 + return ti.Before(&tj) 520 + }) 521 + return items, nil 522 + } 523 + 524 + func (p *tektonProvider) streamTaskRunLogs( 525 + ctx context.Context, 526 + out chan<- LogLine, 527 + ref TektonRunRef, 528 + tr unstructured.Unstructured, 529 + stepID int, 530 + ) { 531 + pods, err := p.podsForTaskRun(ctx, ref.Namespace, tr.GetName()) 532 + if err != nil { 533 + p.log.Debug("list pods for TaskRun", "err", err, 534 + "task_run", tr.GetName(), "pipeline_run", ref.PipelineRunName, 535 + ) 536 + return 537 + } 538 + for _, pod := range pods { 539 + for _, c := range append(pod.Spec.InitContainers, pod.Spec.Containers...) { 540 + req := p.kube.CoreV1().Pods(ref.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{ 541 + Container: c.Name, 542 + }) 543 + rc, err := req.Stream(ctx) 544 + if err != nil { 545 + p.log.Debug("stream pod logs", "err", err, 546 + "pod", pod.Name, "container", c.Name, 547 + ) 548 + continue 549 + } 550 + p.sendReaderLines(ctx, out, rc, stepID) 551 + _ = rc.Close() 552 + } 553 + } 554 + } 555 + 556 + func (p *tektonProvider) podsForTaskRun(ctx context.Context, namespace, taskRun string) ([]corev1.Pod, error) { 557 + sel := labels.Set{"tekton.dev/taskRun": taskRun}.String() 558 + list, err := p.kube.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ 559 + LabelSelector: sel, 560 + }) 561 + if err != nil { 562 + return nil, fmt.Errorf("list pods: %w", err) 563 + } 564 + pods := append([]corev1.Pod(nil), list.Items...) 565 + sort.Slice(pods, func(i, j int) bool { 566 + return pods[i].CreationTimestamp.Before(&pods[j].CreationTimestamp) 567 + }) 568 + return pods, nil 569 + } 570 + 571 + func (p *tektonProvider) sendReaderLines( 572 + ctx context.Context, 573 + out chan<- LogLine, 574 + rc io.Reader, 575 + stepID int, 576 + ) { 577 + scanner := bufio.NewScanner(rc) 578 + for scanner.Scan() { 579 + if !sendLine(ctx, out, LogLine{ 580 + Kind: LogKindData, 581 + Time: time.Now(), 582 + Content: scanner.Text() + "\n", 583 + StepId: stepID, 584 + Stream: "stdout", 585 + }) { 586 + return 587 + } 588 + } 589 + if err := scanner.Err(); err != nil { 590 + p.log.Debug("scan pod log", "err", err) 591 + } 592 + } 593 + 594 + func (p *tektonProvider) publishStatus( 595 + ctx context.Context, 596 + pipelineURI, workflow, status, runName string, 597 + errMsg *string, 598 + exitCode *int64, 599 + ) error { 600 + rec := tangled.PipelineStatus{ 601 + LexiconTypeID: tangled.PipelineStatusNSID, 602 + Pipeline: pipelineURI, 603 + Workflow: workflow, 604 + Status: status, 605 + CreatedAt: time.Now().UTC().Format(time.RFC3339), 606 + Error: errMsg, 607 + ExitCode: exitCode, 608 + } 609 + body, err := json.Marshal(rec) 610 + if err != nil { 611 + return fmt.Errorf("marshal pipeline.status: %w", err) 612 + } 613 + rkey := fmt.Sprintf("tk-%s-%s-%d", runName, status, time.Now().UnixNano()) 614 + if _, err := p.br.Publish(ctx, rkey, tangled.PipelineStatusNSID, body); err != nil { 615 + return fmt.Errorf("publish pipeline.status: %w", err) 616 + } 617 + return nil 618 + } 619 + 620 + func tektonPipelineRunName(knot, pipelineRkey, workflow, commit, branch string) string { 621 + h := sha256.Sum256([]byte(strings.Join( 622 + []string{knot, pipelineRkey, workflow, commit, branch}, "\x00", 623 + ))) 624 + suffix := hex.EncodeToString(h[:])[:12] 625 + base := dnsLabel("tack-" + workflow) 626 + maxBase := 63 - len(suffix) - 1 627 + if len(base) > maxBase { 628 + base = strings.TrimRight(base[:maxBase], "-") 629 + } 630 + if base == "" { 631 + base = "tack" 632 + } 633 + return base + "-" + suffix 634 + } 635 + 636 + func dnsLabel(s string) string { 637 + var b strings.Builder 638 + lastDash := false 639 + for _, r := range strings.ToLower(s) { 640 + ok := unicode.IsLetter(r) || unicode.IsDigit(r) 641 + if ok { 642 + b.WriteRune(r) 643 + lastDash = false 644 + continue 645 + } 646 + if !lastDash { 647 + b.WriteByte('-') 648 + lastDash = true 649 + } 650 + } 651 + return strings.Trim(b.String(), "-") 652 + } 653 + 654 + func labelValue(s string) string { 655 + v := dnsLabel(s) 656 + if len(v) > 63 { 657 + v = strings.TrimRight(v[:63], "-") 658 + } 659 + if v == "" { 660 + return "unknown" 661 + } 662 + return v 663 + }
+291
provider_tekton_test.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "log/slog" 8 + "testing" 9 + "time" 10 + 11 + corev1 "k8s.io/api/core/v1" 12 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 + "k8s.io/apimachinery/pkg/runtime" 15 + runtimeschema "k8s.io/apimachinery/pkg/runtime/schema" 16 + dynamicfake "k8s.io/client-go/dynamic/fake" 17 + kubefake "k8s.io/client-go/kubernetes/fake" 18 + "tangled.org/core/api/tangled" 19 + ) 20 + 21 + func newTektonTestProvider(t *testing.T, objs ...runtime.Object) (*tektonProvider, *store, *broker) { 22 + t.Helper() 23 + st := newTestStore(t) 24 + br := newBroker(st) 25 + dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds( 26 + runtime.NewScheme(), 27 + map[runtimeschema.GroupVersionResource]string{ 28 + pipelineRunsGVR: "PipelineRunList", 29 + taskRunsGVR: "TaskRunList", 30 + }, 31 + objs..., 32 + ) 33 + kube := kubefake.NewSimpleClientset() 34 + p := newTektonProvider(br, st, dyn, kube, "ci", slog.Default()) 35 + return p, st, br 36 + } 37 + 38 + func TestTektonWorkflowConfig(t *testing.T) { 39 + raw := "tack:\n tekton:\n pipeline: repo-ci\n service_account: runner\n params:\n image: example/app\n" 40 + cfg, err := parseTektonWorkflowConfig(raw) 41 + if err != nil { 42 + t.Fatalf("parse: %v", err) 43 + } 44 + if cfg.Pipeline != "repo-ci" || cfg.ServiceAccount != "runner" { 45 + t.Fatalf("cfg mismatch: %+v", cfg) 46 + } 47 + if got := cfg.Params["image"]; got != "example/app" { 48 + t.Fatalf("params[image] = %q", got) 49 + } 50 + 51 + if _, err := parseTektonWorkflowConfig("tack:\n tekton: {}\n"); err == nil { 52 + t.Fatal("missing pipeline should fail") 53 + } 54 + } 55 + 56 + func TestTektonBuildPipelineRun(t *testing.T) { 57 + cfg := &tektonWorkflowConfig{ 58 + Pipeline: "repo-ci", 59 + ServiceAccount: "runner", 60 + Params: map[string]string{ 61 + "image": "example/app", 62 + }, 63 + } 64 + name := tektonPipelineRunName("knot.example.com", "rkey-1", "ci.yml", "abcdef", "main") 65 + if len(name) > 63 || name == "" { 66 + t.Fatalf("bad generated name: %q", name) 67 + } 68 + 69 + obj := buildTektonPipelineRun("ci", name, cfg, 70 + "knot.example.com", "rkey-1", "did:plc:actor", "abcdef", "main", 71 + &tangled.Pipeline_Workflow{Name: "ci.yml"}, 72 + ) 73 + if obj.GetAPIVersion() != tektonAPIVersion || obj.GetKind() != tektonRunKind { 74 + t.Fatalf("type meta mismatch: %s %s", obj.GetAPIVersion(), obj.GetKind()) 75 + } 76 + pipeline, _, _ := unstructured.NestedString(obj.Object, "spec", "pipelineRef", "name") 77 + if pipeline != "repo-ci" { 78 + t.Fatalf("pipelineRef.name = %q", pipeline) 79 + } 80 + sa, _, _ := unstructured.NestedString(obj.Object, "spec", "serviceAccountName") 81 + if sa != "runner" { 82 + t.Fatalf("serviceAccountName = %q", sa) 83 + } 84 + params, _, _ := unstructured.NestedSlice(obj.Object, "spec", "params") 85 + if len(params) != 1 { 86 + t.Fatalf("params = %+v", params) 87 + } 88 + if obj.GetAnnotations()[tektonAnnotationActor] != "did:plc:actor" || 89 + obj.GetAnnotations()[tektonAnnotationCommit] != "abcdef" { 90 + t.Fatalf("annotations missing identity: %+v", obj.GetAnnotations()) 91 + } 92 + } 93 + 94 + func TestTektonStatusMapping(t *testing.T) { 95 + tests := []struct { 96 + name string 97 + cond string 98 + reason string 99 + status string 100 + terminal bool 101 + ok bool 102 + }{ 103 + {name: "unknown", cond: "Unknown", status: "running", ok: true}, 104 + {name: "success", cond: "True", status: "success", terminal: true, ok: true}, 105 + {name: "failed", cond: "False", reason: "Failed", status: "failed", terminal: true, ok: true}, 106 + {name: "cancelled", cond: "False", reason: "PipelineRunCancelled", status: "cancelled", terminal: true, ok: true}, 107 + {name: "stopped", cond: "False", reason: "PipelineRunStopped", status: "cancelled", terminal: true, ok: true}, 108 + } 109 + for _, tt := range tests { 110 + t.Run(tt.name, func(t *testing.T) { 111 + obj := tektonStatusObject(tt.cond, tt.reason) 112 + status, terminal, ok := mapTektonPipelineRunStatus(obj) 113 + if status != tt.status || terminal != tt.terminal || ok != tt.ok { 114 + t.Fatalf("got %q/%v/%v; want %q/%v/%v", 115 + status, terminal, ok, tt.status, tt.terminal, tt.ok) 116 + } 117 + }) 118 + } 119 + } 120 + 121 + func TestTektonSpawnCreatesPipelineRun(t *testing.T) { 122 + p, st, _ := newTektonTestProvider(t) 123 + ctx, cancel := context.WithCancel(context.Background()) 124 + defer cancel() 125 + 126 + trigger := &tangled.Pipeline_TriggerMetadata{ 127 + Push: &tangled.Pipeline_PushTriggerData{ 128 + NewSha: "abcdef0123", 129 + Ref: "refs/heads/main", 130 + }, 131 + } 132 + p.Spawn(ctx, "knot.example.com", "rkey-1", "did:plc:actor", trigger, 133 + []*tangled.Pipeline_Workflow{{Name: "ci.yml", 134 + Raw: "tack:\n tekton:\n pipeline: repo-ci\n"}}, 135 + ) 136 + 137 + ref := waitTektonRef(t, st, "knot.example.com", "rkey-1", "ci.yml") 138 + if ref.Namespace != "ci" || ref.PipelineName != "repo-ci" { 139 + t.Fatalf("ref mismatch: %+v", ref) 140 + } 141 + 142 + obj, err := p.dyn.Resource(pipelineRunsGVR).Namespace("ci"). 143 + Get(context.Background(), ref.PipelineRunName, metav1.GetOptions{}) 144 + if err != nil { 145 + t.Fatalf("get PipelineRun: %v", err) 146 + } 147 + pipeline, _, _ := unstructured.NestedString(obj.Object, "spec", "pipelineRef", "name") 148 + if pipeline != "repo-ci" { 149 + t.Fatalf("pipelineRef.name = %q", pipeline) 150 + } 151 + 152 + rows, err := st.EventsAfter(context.Background(), 0) 153 + if err != nil { 154 + t.Fatalf("EventsAfter: %v", err) 155 + } 156 + if len(rows) != 1 { 157 + t.Fatalf("got %d events, want 1", len(rows)) 158 + } 159 + var rec tangled.PipelineStatus 160 + if err := json.Unmarshal(rows[0].EventJSON, &rec); err != nil { 161 + t.Fatalf("decode status: %v", err) 162 + } 163 + if rec.Status != "pending" || rec.Workflow != "ci.yml" { 164 + t.Fatalf("bad pending status: %+v", rec) 165 + } 166 + } 167 + 168 + func TestTektonSpawnAlreadyExists(t *testing.T) { 169 + name := tektonPipelineRunName("knot.example.com", "rkey-1", "ci.yml", "abcdef0123", "main") 170 + existing := buildTektonPipelineRun("ci", name, 171 + &tektonWorkflowConfig{Pipeline: "repo-ci"}, 172 + "knot.example.com", "rkey-1", "did:plc:actor", "abcdef0123", "main", 173 + &tangled.Pipeline_Workflow{Name: "ci.yml"}, 174 + ) 175 + existing.SetUID("uid-1") 176 + p, st, _ := newTektonTestProvider(t, existing) 177 + ctx, cancel := context.WithCancel(context.Background()) 178 + defer cancel() 179 + 180 + p.Spawn(ctx, "knot.example.com", "rkey-1", "did:plc:actor", 181 + &tangled.Pipeline_TriggerMetadata{Push: &tangled.Pipeline_PushTriggerData{ 182 + NewSha: "abcdef0123", 183 + Ref: "refs/heads/main", 184 + }}, 185 + []*tangled.Pipeline_Workflow{{Name: "ci.yml", 186 + Raw: "tack:\n tekton:\n pipeline: repo-ci\n"}}, 187 + ) 188 + 189 + ref := waitTektonRef(t, st, "knot.example.com", "rkey-1", "ci.yml") 190 + if ref.PipelineRunName != name || ref.PipelineRunUID != "uid-1" { 191 + t.Fatalf("ref mismatch: %+v", ref) 192 + } 193 + } 194 + 195 + func TestTektonLogsLookup(t *testing.T) { 196 + p, st, _ := newTektonTestProvider(t) 197 + ctx := context.Background() 198 + if _, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml"); !errors.Is(err, ErrLogsNotFound) { 199 + t.Fatalf("logs before mapping err = %v; want ErrLogsNotFound", err) 200 + } 201 + ref := TektonRunRef{ 202 + Knot: "knot.example.com", 203 + PipelineRkey: "rkey-1", 204 + Workflow: "ci.yml", 205 + Namespace: "ci", 206 + PipelineRunName: "run-1", 207 + PipelineRunUID: "uid-1", 208 + PipelineName: "repo-ci", 209 + PipelineURI: pipelineATURI("knot.example.com", "rkey-1"), 210 + } 211 + if err := st.InsertTektonRun(ctx, ref); err != nil { 212 + t.Fatalf("insert ref: %v", err) 213 + } 214 + if _, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml"); !errors.Is(err, ErrLogsNotFound) { 215 + t.Fatalf("logs before TaskRuns err = %v; want ErrLogsNotFound", err) 216 + } 217 + 218 + taskRun := &unstructured.Unstructured{Object: map[string]interface{}{ 219 + "apiVersion": "tekton.dev/v1", 220 + "kind": "TaskRun", 221 + "metadata": map[string]interface{}{ 222 + "name": "task-1", 223 + "namespace": "ci", 224 + "labels": map[string]interface{}{ 225 + "tekton.dev/pipelineRun": "run-1", 226 + }, 227 + }, 228 + }} 229 + _, err := p.dyn.Resource(taskRunsGVR).Namespace("ci"). 230 + Create(ctx, taskRun, metav1.CreateOptions{}) 231 + if err != nil { 232 + t.Fatalf("create TaskRun: %v", err) 233 + } 234 + _, err = p.kube.CoreV1().Pods("ci").Create(ctx, &corev1.Pod{ 235 + ObjectMeta: metav1.ObjectMeta{ 236 + Name: "pod-1", 237 + Namespace: "ci", 238 + Labels: map[string]string{ 239 + "tekton.dev/taskRun": "task-1", 240 + }, 241 + }, 242 + Spec: corev1.PodSpec{ 243 + Containers: []corev1.Container{{Name: "step-test", Image: "busybox"}}, 244 + }, 245 + }, metav1.CreateOptions{}) 246 + if err != nil { 247 + t.Fatalf("create pod: %v", err) 248 + } 249 + 250 + ch, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml") 251 + if err != nil { 252 + t.Fatalf("Logs after pods: %v", err) 253 + } 254 + var got []LogLine 255 + for line := range ch { 256 + got = append(got, line) 257 + } 258 + if len(got) < 2 || got[0].StepStatus != StepStatusStart || 259 + got[len(got)-1].StepStatus != StepStatusEnd { 260 + t.Fatalf("log frames = %+v", got) 261 + } 262 + } 263 + 264 + func tektonStatusObject(condStatus, reason string) *unstructured.Unstructured { 265 + return &unstructured.Unstructured{Object: map[string]interface{}{ 266 + "status": map[string]interface{}{ 267 + "conditions": []interface{}{map[string]interface{}{ 268 + "type": "Succeeded", 269 + "status": condStatus, 270 + "reason": reason, 271 + }}, 272 + }, 273 + }} 274 + } 275 + 276 + func waitTektonRef(t *testing.T, st *store, knot, rkey, workflow string) *TektonRunRef { 277 + t.Helper() 278 + deadline := time.Now().Add(2 * time.Second) 279 + for time.Now().Before(deadline) { 280 + ref, err := st.LookupTektonRunByTuple(context.Background(), knot, rkey, workflow) 281 + if err != nil { 282 + t.Fatalf("lookup: %v", err) 283 + } 284 + if ref != nil { 285 + return ref 286 + } 287 + time.Sleep(20 * time.Millisecond) 288 + } 289 + t.Fatal("tekton run row not persisted within deadline") 290 + return nil 291 + }
+69
store.go
··· 526 526 PipelineURI string 527 527 } 528 528 529 + // TektonRunRef is the persisted link from a Tangled workflow tuple 530 + // to the in-cluster PipelineRun tack created for it. The tuple is the 531 + // user-facing identity the appview knows; namespace/name/uid are the 532 + // Kubernetes identity needed for status watching and log lookup. 533 + type TektonRunRef struct { 534 + Knot string 535 + PipelineRkey string 536 + Workflow string 537 + Namespace string 538 + PipelineRunName string 539 + PipelineRunUID string 540 + PipelineName string 541 + PipelineURI string 542 + } 543 + 529 544 // InsertBuildkiteBuild records that a Buildkite build was created on 530 545 // behalf of the given (knot, pipelineRkey, workflow) tuple. Uses 531 546 // INSERT OR REPLACE so that an unlikely build-uuid collision (or a ··· 624 639 } 625 640 if err != nil { 626 641 return nil, fmt.Errorf("lookup buildkite_build by tuple: %w", err) 642 + } 643 + return &ref, nil 644 + } 645 + 646 + // InsertTektonRun records the latest PipelineRun created for a Tangled 647 + // workflow tuple. Reusing the tuple as the primary key intentionally 648 + // makes /logs resolve to the newest run for that workflow identity. 649 + func (s *store) InsertTektonRun(ctx context.Context, ref TektonRunRef) error { 650 + now := time.Now().UTC() 651 + _, err := s.db.ExecContext(ctx, 652 + `INSERT INTO tekton_runs ( 653 + knot, pipeline_rkey, workflow, 654 + namespace, pipeline_run_name, pipeline_run_uid, 655 + pipeline_name, pipeline_uri, created_at, created_unix_ns 656 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 657 + ON CONFLICT(knot, pipeline_rkey, workflow) DO UPDATE SET 658 + namespace = excluded.namespace, 659 + pipeline_run_name = excluded.pipeline_run_name, 660 + pipeline_run_uid = excluded.pipeline_run_uid, 661 + pipeline_name = excluded.pipeline_name, 662 + pipeline_uri = excluded.pipeline_uri, 663 + created_at = excluded.created_at, 664 + created_unix_ns = excluded.created_unix_ns`, 665 + ref.Knot, ref.PipelineRkey, ref.Workflow, 666 + ref.Namespace, ref.PipelineRunName, ref.PipelineRunUID, 667 + ref.PipelineName, ref.PipelineURI, now.Format(time.RFC3339Nano), now.UnixNano(), 668 + ) 669 + if err != nil { 670 + return fmt.Errorf("insert tekton_run: %w", err) 671 + } 672 + return nil 673 + } 674 + 675 + // LookupTektonRunByTuple resolves the appview's path-based identity to 676 + // the concrete PipelineRun tack created in Kubernetes. 677 + func (s *store) LookupTektonRunByTuple(ctx context.Context, knot, pipelineRkey, workflow string) (*TektonRunRef, error) { 678 + var ref TektonRunRef 679 + err := s.db.QueryRowContext(ctx, 680 + `SELECT knot, pipeline_rkey, workflow, 681 + namespace, pipeline_run_name, pipeline_run_uid, 682 + pipeline_name, pipeline_uri 683 + FROM tekton_runs 684 + WHERE knot = ? AND pipeline_rkey = ? AND workflow = ?`, 685 + knot, pipelineRkey, workflow, 686 + ).Scan( 687 + &ref.Knot, &ref.PipelineRkey, &ref.Workflow, 688 + &ref.Namespace, &ref.PipelineRunName, &ref.PipelineRunUID, 689 + &ref.PipelineName, &ref.PipelineURI, 690 + ) 691 + if errors.Is(err, sql.ErrNoRows) { 692 + return nil, nil 693 + } 694 + if err != nil { 695 + return nil, fmt.Errorf("lookup tekton_run by tuple: %w", err) 627 696 } 628 697 return &ref, nil 629 698 }
+21
store_migrate.go
··· 121 121 ); 122 122 CREATE INDEX IF NOT EXISTS buildkite_builds_lookup 123 123 ON buildkite_builds (knot, pipeline_rkey, workflow); 124 + 125 + -- Mapping from a Tangled workflow tuple to the latest Tekton 126 + -- PipelineRun tack created for it. Unlike Buildkite webhooks, Tekton 127 + -- status observation happens in-process, so the primary read path is 128 + -- /logs resolving (knot, pipeline_rkey, workflow) back to the concrete 129 + -- PipelineRun whose TaskRuns and pods hold output. 130 + CREATE TABLE IF NOT EXISTS tekton_runs ( 131 + knot TEXT NOT NULL, 132 + pipeline_rkey TEXT NOT NULL, 133 + workflow TEXT NOT NULL, 134 + namespace TEXT NOT NULL, 135 + pipeline_run_name TEXT NOT NULL, 136 + pipeline_run_uid TEXT NOT NULL, 137 + pipeline_name TEXT NOT NULL, 138 + pipeline_uri TEXT NOT NULL, 139 + created_at TEXT NOT NULL, 140 + created_unix_ns INTEGER NOT NULL, 141 + PRIMARY KEY (knot, pipeline_rkey, workflow) 142 + ); 143 + CREATE INDEX IF NOT EXISTS tekton_runs_uid 144 + ON tekton_runs (pipeline_run_uid); 124 145 ` 125 146 126 147 // migrate applies the schema. Safe to call repeatedly.