···
110
110
its own doc per provider:
111
111
112
112
* [Buildkite](docs/buildkite.md)
113
113
+
* [Tekton](docs/tekton.md)
···
1
1
+
# Tekton
2
2
+
3
3
+
The Tekton provider runs only in Kubernetes. Tack receives Tangled
4
4
+
pipeline triggers, creates a Tekton `PipelineRun` for an existing
5
5
+
in-cluster `Pipeline`, watches that `PipelineRun`, and publishes
6
6
+
`sh.tangled.pipeline.status` records back to Tangled.
7
7
+
8
8
+
Tekton Triggers are intentionally not used. Tack already performs the
9
9
+
event-to-run translation, and Tekton's native execution object is the
10
10
+
`PipelineRun`.
11
11
+
12
12
+
## Required cluster setup
13
13
+
14
14
+
* Tekton Pipelines is installed in the cluster.
15
15
+
* Tack is deployed inside the same cluster.
16
16
+
* The target Tekton `Pipeline` objects already exist in the namespace
17
17
+
tack is configured to use.
18
18
+
* Tack's Kubernetes service account has RBAC to:
19
19
+
* create, get, list, and watch `tekton.dev` `pipelineruns`
20
20
+
* get, list, and watch `tekton.dev` `taskruns`
21
21
+
* get and list pods
22
22
+
* get pod logs via `pods/log`
23
23
+
24
24
+
Example RBAC:
25
25
+
26
26
+
```yaml
27
27
+
apiVersion: rbac.authorization.k8s.io/v1
28
28
+
kind: Role
29
29
+
metadata:
30
30
+
name: tack-tekton
31
31
+
namespace: ci
32
32
+
rules:
33
33
+
- apiGroups: ["tekton.dev"]
34
34
+
resources: ["pipelineruns"]
35
35
+
verbs: ["create", "get", "list", "watch"]
36
36
+
- apiGroups: ["tekton.dev"]
37
37
+
resources: ["taskruns"]
38
38
+
verbs: ["get", "list", "watch"]
39
39
+
- apiGroups: [""]
40
40
+
resources: ["pods"]
41
41
+
verbs: ["get", "list"]
42
42
+
- apiGroups: [""]
43
43
+
resources: ["pods/log"]
44
44
+
verbs: ["get"]
45
45
+
```
46
46
+
47
47
+
## Configure Tack
48
48
+
49
49
+
| Env var | Description |
50
50
+
| ------------------------ | --------------------------------------------------------- |
51
51
+
| `TACK_TEKTON_ENABLED` | Set to `1` to enable the Tekton provider |
52
52
+
| `TACK_TEKTON_NAMESPACE` | Namespace for created `PipelineRun`s (default `default`) |
53
53
+
54
54
+
The provider uses Kubernetes in-cluster service account credentials.
55
55
+
It will not run from a local kubeconfig.
56
56
+
57
57
+
## Naming
58
58
+
59
59
+
There are three separate names:
60
60
+
61
61
+
* Tack workflow name: the Tangled workflow filename/name, e.g. `ci.yml`.
62
62
+
This remains the Tangled-facing workflow identity in status records.
63
63
+
* Tekton `Pipeline` name: the existing in-cluster pipeline definition,
64
64
+
e.g. `repo-ci`. This is written to `spec.pipelineRef.name`.
65
65
+
* Tekton `PipelineRun` name: generated by tack per trigger/workflow,
66
66
+
e.g. `tack-ci-yml-<short-hash>`. This is the concrete execution
67
67
+
object tack watches and stores.
68
68
+
69
69
+
## Workflow YAML
70
70
+
71
71
+
Only the provider and target pipeline are required:
72
72
+
73
73
+
```yaml
74
74
+
tack:
75
75
+
tekton:
76
76
+
pipeline: repo-ci
77
77
+
```
78
78
+
79
79
+
Optional fields:
80
80
+
81
81
+
```yaml
82
82
+
tack:
83
83
+
tekton:
84
84
+
pipeline: repo-ci
85
85
+
service_account: pipeline-runner
86
86
+
params:
87
87
+
image: example/app
88
88
+
```
89
89
+
90
90
+
`params` are forwarded as string Tekton params. Tack also stores the
91
91
+
knot, pipeline rkey, workflow name, actor DID, commit, and branch as
92
92
+
`PipelineRun` annotations, so operators can inspect the Kubernetes
93
93
+
object and connect it back to the Tangled trigger.
94
94
+
95
95
+
## Example Pipeline
96
96
+
97
97
+
```yaml
98
98
+
apiVersion: tekton.dev/v1
99
99
+
kind: Pipeline
100
100
+
metadata:
101
101
+
name: repo-ci
102
102
+
namespace: ci
103
103
+
spec:
104
104
+
params:
105
105
+
- name: image
106
106
+
type: string
107
107
+
tasks:
108
108
+
- name: test
109
109
+
taskSpec:
110
110
+
params:
111
111
+
- name: image
112
112
+
type: string
113
113
+
steps:
114
114
+
- name: test
115
115
+
image: golang:1.25
116
116
+
script: |
117
117
+
set -eu
118
118
+
echo "building $(params.image)"
119
119
+
go test ./...
120
120
+
workspaces: []
121
121
+
params:
122
122
+
- name: image
123
123
+
value: $(params.image)
124
124
+
```
125
125
+
126
126
+
Detailed CI behavior belongs in the in-cluster `Pipeline`. The Tangled
127
127
+
workflow YAML should stay small: select `tekton`, pick the target
128
128
+
pipeline, and pass only the small set of params that pipeline expects.
···
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
11
+
k8s.io/api v0.35.3
12
12
+
k8s.io/apimachinery v0.35.3
13
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
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
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
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
49
+
github.com/go-openapi/jsonpointer v0.21.0 // indirect
50
50
+
github.com/go-openapi/jsonreference v0.20.2 // indirect
51
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
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
79
+
github.com/josharian/intern v1.0.0 // indirect
80
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
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
89
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
90
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
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
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
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
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
140
+
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
141
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
144
+
k8s.io/klog/v2 v2.130.1 // indirect
145
145
+
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
146
146
+
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
123
147
lukechampine.com/blake3 v1.4.1 // indirect
148
148
+
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
149
149
+
sigs.k8s.io/randfill v1.0.0 // indirect
150
150
+
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
151
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.
···
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
4
+
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
5
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
67
+
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
68
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
79
+
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
80
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
97
+
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
98
98
+
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
99
99
+
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
100
100
+
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
101
101
+
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
102
102
+
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
103
103
+
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
104
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
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
109
+
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
110
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
129
+
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
130
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
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
141
+
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
142
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
207
+
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
208
208
+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
209
209
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
210
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
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
233
+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
234
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
247
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
248
248
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
249
249
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
250
250
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
251
251
+
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
252
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
284
+
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
285
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
259
-
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
260
260
-
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
296
296
+
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
297
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
345
+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
346
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
350
+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
351
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
374
+
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
375
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
416
+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
417
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
438
+
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
439
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
461
+
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
462
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
510
+
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
511
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
541
+
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
542
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
566
+
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
567
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
569
+
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
570
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
583
+
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
584
584
+
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
585
585
+
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
586
586
+
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
587
587
+
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
588
588
+
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
589
589
+
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
590
590
+
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
591
591
+
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
592
592
+
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
593
593
+
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
594
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
597
+
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
598
598
+
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
599
599
+
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
600
600
+
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
601
601
+
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
602
602
+
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
603
603
+
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
604
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=
···
46
46
BuildkiteOrg string
47
47
BuildkiteWebhookSecret string
48
48
BuildkiteWebhookMode buildkite.WebhookMode
49
49
+
50
50
+
// Tekton mode is explicit because it only works from inside a
51
51
+
// Kubernetes cluster. When enabled, tack creates PipelineRuns in
52
52
+
// TektonNamespace using its pod's service account credentials.
53
53
+
TektonEnabled bool
54
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
71
+
TektonEnabled: os.Getenv("TACK_TEKTON_ENABLED") == "1",
72
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
110
+
}
111
111
+
if cfg.TektonEnabled && cfg.TektonNamespace == "" {
112
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
203
+
)
204
204
+
}
205
205
+
if cfg.TektonEnabled {
206
206
+
tkProvider, err := newInClusterTektonProvider(
207
207
+
br, st,
208
208
+
cfg.TektonNamespace,
209
209
+
logger,
210
210
+
)
211
211
+
if err != nil {
212
212
+
logger.Error("failed to configure tekton provider", "err", err)
213
213
+
os.Exit(1)
214
214
+
}
215
215
+
providers["tekton"] = tkProvider
216
216
+
logger.Info("tekton provider enabled",
217
217
+
"namespace", cfg.TektonNamespace,
192
218
)
193
219
}
194
220
provider := newProviderRouter(logger, providers)
···
1
1
+
package main
2
2
+
3
3
+
// tektonProvider implements Provider by creating Tekton PipelineRuns
4
4
+
// directly inside the Kubernetes cluster. Tack already receives and
5
5
+
// authorizes Tangled pipeline triggers, so adding Tekton Triggers would
6
6
+
// duplicate the event-to-run translation layer instead of simplifying it.
7
7
+
8
8
+
import (
9
9
+
"bufio"
10
10
+
"context"
11
11
+
"crypto/sha256"
12
12
+
"encoding/hex"
13
13
+
"encoding/json"
14
14
+
"errors"
15
15
+
"fmt"
16
16
+
"io"
17
17
+
"log/slog"
18
18
+
"sort"
19
19
+
"strings"
20
20
+
"time"
21
21
+
"unicode"
22
22
+
23
23
+
corev1 "k8s.io/api/core/v1"
24
24
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25
25
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26
26
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27
27
+
"k8s.io/apimachinery/pkg/fields"
28
28
+
"k8s.io/apimachinery/pkg/labels"
29
29
+
runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
30
30
+
"k8s.io/client-go/dynamic"
31
31
+
"k8s.io/client-go/kubernetes"
32
32
+
"k8s.io/client-go/rest"
33
33
+
"tangled.org/core/api/tangled"
34
34
+
35
35
+
"go.yaml.in/yaml/v2"
36
36
+
)
37
37
+
38
38
+
const (
39
39
+
tektonAPIVersion = "tekton.dev/v1"
40
40
+
tektonRunKind = "PipelineRun"
41
41
+
42
42
+
tektonLabelManagedBy = "tack.mitchellh.com/managed-by"
43
43
+
tektonLabelPipelineRkey = "tack.mitchellh.com/pipeline-rkey"
44
44
+
tektonLabelWorkflow = "tack.mitchellh.com/workflow"
45
45
+
46
46
+
tektonAnnotationKnot = "tack.mitchellh.com/knot"
47
47
+
tektonAnnotationPipelineRkey = "tack.mitchellh.com/pipeline-rkey"
48
48
+
tektonAnnotationWorkflow = "tack.mitchellh.com/workflow"
49
49
+
tektonAnnotationActor = "tack.mitchellh.com/actor"
50
50
+
tektonAnnotationCommit = "tack.mitchellh.com/commit"
51
51
+
tektonAnnotationBranch = "tack.mitchellh.com/branch"
52
52
+
)
53
53
+
54
54
+
var (
55
55
+
pipelineRunsGVR = runtimeschema.GroupVersionResource{
56
56
+
Group: "tekton.dev", Version: "v1", Resource: "pipelineruns",
57
57
+
}
58
58
+
taskRunsGVR = runtimeschema.GroupVersionResource{
59
59
+
Group: "tekton.dev", Version: "v1", Resource: "taskruns",
60
60
+
}
61
61
+
)
62
62
+
63
63
+
// tektonWorkflowConfig is the Tekton-specific subset of workflow YAML.
64
64
+
// `pipeline` names an existing in-cluster Tekton Pipeline. Params are
65
65
+
// deliberately string-only in v1: tack is meant to select an existing
66
66
+
// runner and pass a small amount of routing data, not mirror Tekton's
67
67
+
// entire PipelineRun API.
68
68
+
type tektonWorkflowConfig struct {
69
69
+
Pipeline string `yaml:"pipeline"`
70
70
+
ServiceAccount string `yaml:"service_account"`
71
71
+
Params map[string]string `yaml:"params"`
72
72
+
}
73
73
+
74
74
+
type tektonWorkflowDoc struct {
75
75
+
Tack struct {
76
76
+
Tekton tektonWorkflowConfig `yaml:"tekton"`
77
77
+
} `yaml:"tack"`
78
78
+
}
79
79
+
80
80
+
// parseTektonWorkflowConfig decodes `tack.tekton` from a workflow body.
81
81
+
func parseTektonWorkflowConfig(raw string) (*tektonWorkflowConfig, error) {
82
82
+
if strings.TrimSpace(raw) == "" {
83
83
+
return nil, errors.New("workflow body is empty")
84
84
+
}
85
85
+
var doc tektonWorkflowDoc
86
86
+
if err := yaml.Unmarshal([]byte(raw), &doc); err != nil {
87
87
+
return nil, fmt.Errorf("parse workflow yaml: %w", err)
88
88
+
}
89
89
+
cfg := doc.Tack.Tekton
90
90
+
if cfg.Pipeline == "" {
91
91
+
return nil, errors.New("workflow yaml: `tack.tekton.pipeline` is required")
92
92
+
}
93
93
+
return &cfg, nil
94
94
+
}
95
95
+
96
96
+
type tektonProvider struct {
97
97
+
br *broker
98
98
+
st *store
99
99
+
log *slog.Logger
100
100
+
dyn dynamic.Interface
101
101
+
kube kubernetes.Interface
102
102
+
namespace string
103
103
+
}
104
104
+
105
105
+
var _ Provider = (*tektonProvider)(nil)
106
106
+
107
107
+
func newTektonProvider(
108
108
+
br *broker,
109
109
+
st *store,
110
110
+
dyn dynamic.Interface,
111
111
+
kube kubernetes.Interface,
112
112
+
namespace string,
113
113
+
log *slog.Logger,
114
114
+
) *tektonProvider {
115
115
+
return &tektonProvider{
116
116
+
br: br,
117
117
+
st: st,
118
118
+
log: log.With("component", "provider", "kind", "tekton"),
119
119
+
dyn: dyn,
120
120
+
kube: kube,
121
121
+
namespace: namespace,
122
122
+
}
123
123
+
}
124
124
+
125
125
+
func newInClusterTektonProvider(
126
126
+
br *broker,
127
127
+
st *store,
128
128
+
namespace string,
129
129
+
log *slog.Logger,
130
130
+
) (*tektonProvider, error) {
131
131
+
cfg, err := rest.InClusterConfig()
132
132
+
if err != nil {
133
133
+
return nil, fmt.Errorf("load in-cluster kubernetes config: %w", err)
134
134
+
}
135
135
+
dyn, err := dynamic.NewForConfig(cfg)
136
136
+
if err != nil {
137
137
+
return nil, fmt.Errorf("create dynamic kubernetes client: %w", err)
138
138
+
}
139
139
+
kube, err := kubernetes.NewForConfig(cfg)
140
140
+
if err != nil {
141
141
+
return nil, fmt.Errorf("create kubernetes client: %w", err)
142
142
+
}
143
143
+
return newTektonProvider(br, st, dyn, kube, namespace, log), nil
144
144
+
}
145
145
+
146
146
+
func (p *tektonProvider) Spawn(
147
147
+
ctx context.Context,
148
148
+
knot string,
149
149
+
pipelineRkey string,
150
150
+
actor string,
151
151
+
trigger *tangled.Pipeline_TriggerMetadata,
152
152
+
workflows []*tangled.Pipeline_Workflow,
153
153
+
) {
154
154
+
if len(workflows) == 0 {
155
155
+
p.log.Warn("pipeline has no workflows; nothing to spawn",
156
156
+
"knot", knot, "rkey", pipelineRkey,
157
157
+
)
158
158
+
return
159
159
+
}
160
160
+
for _, wf := range workflows {
161
161
+
if wf == nil || wf.Name == "" {
162
162
+
continue
163
163
+
}
164
164
+
wf := wf
165
165
+
go p.spawnWorkflow(ctx, knot, pipelineRkey, actor, trigger, wf)
166
166
+
}
167
167
+
}
168
168
+
169
169
+
func (p *tektonProvider) spawnWorkflow(
170
170
+
ctx context.Context,
171
171
+
knot string,
172
172
+
pipelineRkey string,
173
173
+
actor string,
174
174
+
trigger *tangled.Pipeline_TriggerMetadata,
175
175
+
wf *tangled.Pipeline_Workflow,
176
176
+
) {
177
177
+
logger := p.log.With(
178
178
+
"knot", knot,
179
179
+
"pipeline_rkey", pipelineRkey,
180
180
+
"workflow", wf.Name,
181
181
+
"actor", actor,
182
182
+
)
183
183
+
184
184
+
cfg, err := parseTektonWorkflowConfig(wf.Raw)
185
185
+
if err != nil {
186
186
+
logger.Error("invalid workflow config; refusing to spawn", "err", err)
187
187
+
return
188
188
+
}
189
189
+
commit, branch := triggerCommitAndBranch(trigger)
190
190
+
name := tektonPipelineRunName(knot, pipelineRkey, wf.Name, commit, branch)
191
191
+
pr := buildTektonPipelineRun(
192
192
+
p.namespace, name, cfg, knot, pipelineRkey, actor, commit, branch, wf,
193
193
+
)
194
194
+
195
195
+
runs := p.dyn.Resource(pipelineRunsGVR).Namespace(p.namespace)
196
196
+
created, err := runs.Create(ctx, pr, metav1.CreateOptions{})
197
197
+
if apierrors.IsAlreadyExists(err) {
198
198
+
created, err = runs.Get(ctx, name, metav1.GetOptions{})
199
199
+
}
200
200
+
if err != nil {
201
201
+
logger.Error("create tekton PipelineRun", "err", err,
202
202
+
"namespace", p.namespace, "pipeline_run", name,
203
203
+
"pipeline", cfg.Pipeline,
204
204
+
)
205
205
+
return
206
206
+
}
207
207
+
208
208
+
ref := TektonRunRef{
209
209
+
Knot: knot,
210
210
+
PipelineRkey: pipelineRkey,
211
211
+
Workflow: wf.Name,
212
212
+
Namespace: p.namespace,
213
213
+
PipelineRunName: name,
214
214
+
PipelineRunUID: string(created.GetUID()),
215
215
+
PipelineName: cfg.Pipeline,
216
216
+
PipelineURI: pipelineATURI(knot, pipelineRkey),
217
217
+
}
218
218
+
if err := p.st.InsertTektonRun(ctx, ref); err != nil {
219
219
+
logger.Error("persist tekton run mapping", "err", err,
220
220
+
"pipeline_run", name,
221
221
+
)
222
222
+
return
223
223
+
}
224
224
+
225
225
+
if err := p.publishStatus(ctx, ref.PipelineURI, wf.Name,
226
226
+
"pending", name, nil, nil); err != nil {
227
227
+
logger.Error("publish initial pending status", "err", err)
228
228
+
}
229
229
+
230
230
+
logger.Info("tekton PipelineRun created",
231
231
+
"namespace", p.namespace,
232
232
+
"pipeline", cfg.Pipeline,
233
233
+
"pipeline_run", name,
234
234
+
"uid", ref.PipelineRunUID,
235
235
+
)
236
236
+
go p.watchPipelineRun(ctx, ref)
237
237
+
}
238
238
+
239
239
+
func buildTektonPipelineRun(
240
240
+
namespace, name string,
241
241
+
cfg *tektonWorkflowConfig,
242
242
+
knot, pipelineRkey, actor, commit, branch string,
243
243
+
wf *tangled.Pipeline_Workflow,
244
244
+
) *unstructured.Unstructured {
245
245
+
obj := &unstructured.Unstructured{
246
246
+
Object: map[string]interface{}{
247
247
+
"apiVersion": tektonAPIVersion,
248
248
+
"kind": tektonRunKind,
249
249
+
"metadata": map[string]interface{}{
250
250
+
"name": name,
251
251
+
"namespace": namespace,
252
252
+
"labels": map[string]interface{}{
253
253
+
tektonLabelManagedBy: "tack",
254
254
+
tektonLabelPipelineRkey: labelValue(pipelineRkey),
255
255
+
tektonLabelWorkflow: labelValue(wf.Name),
256
256
+
},
257
257
+
"annotations": map[string]interface{}{
258
258
+
tektonAnnotationKnot: knot,
259
259
+
tektonAnnotationPipelineRkey: pipelineRkey,
260
260
+
tektonAnnotationWorkflow: wf.Name,
261
261
+
tektonAnnotationActor: actor,
262
262
+
tektonAnnotationCommit: commit,
263
263
+
tektonAnnotationBranch: branch,
264
264
+
},
265
265
+
},
266
266
+
"spec": map[string]interface{}{
267
267
+
"pipelineRef": map[string]interface{}{
268
268
+
"name": cfg.Pipeline,
269
269
+
},
270
270
+
},
271
271
+
},
272
272
+
}
273
273
+
spec := obj.Object["spec"].(map[string]interface{})
274
274
+
if cfg.ServiceAccount != "" {
275
275
+
spec["serviceAccountName"] = cfg.ServiceAccount
276
276
+
}
277
277
+
if len(cfg.Params) > 0 {
278
278
+
keys := make([]string, 0, len(cfg.Params))
279
279
+
for key := range cfg.Params {
280
280
+
keys = append(keys, key)
281
281
+
}
282
282
+
sort.Strings(keys)
283
283
+
params := make([]interface{}, 0, len(keys))
284
284
+
for _, key := range keys {
285
285
+
params = append(params, map[string]interface{}{
286
286
+
"name": key,
287
287
+
"value": cfg.Params[key],
288
288
+
})
289
289
+
}
290
290
+
spec["params"] = params
291
291
+
}
292
292
+
return obj
293
293
+
}
294
294
+
295
295
+
func (p *tektonProvider) watchPipelineRun(ctx context.Context, ref TektonRunRef) {
296
296
+
logger := p.log.With(
297
297
+
"knot", ref.Knot,
298
298
+
"pipeline_rkey", ref.PipelineRkey,
299
299
+
"workflow", ref.Workflow,
300
300
+
"namespace", ref.Namespace,
301
301
+
"pipeline_run", ref.PipelineRunName,
302
302
+
)
303
303
+
304
304
+
last := ""
305
305
+
if obj, err := p.dyn.Resource(pipelineRunsGVR).Namespace(ref.Namespace).
306
306
+
Get(ctx, ref.PipelineRunName, metav1.GetOptions{}); err == nil {
307
307
+
status, terminal, ok := mapTektonPipelineRunStatus(obj)
308
308
+
if ok {
309
309
+
last = status
310
310
+
if err := p.publishStatus(ctx, ref.PipelineURI, ref.Workflow,
311
311
+
status, ref.PipelineRunName, nil, nil); err != nil {
312
312
+
logger.Error("publish tekton status", "err", err, "status", status)
313
313
+
}
314
314
+
if terminal {
315
315
+
return
316
316
+
}
317
317
+
}
318
318
+
} else if apierrors.IsNotFound(err) {
319
319
+
logger.Warn("PipelineRun disappeared while watching")
320
320
+
return
321
321
+
} else {
322
322
+
logger.Debug("initial PipelineRun status read", "err", err)
323
323
+
}
324
324
+
325
325
+
w, err := p.dyn.Resource(pipelineRunsGVR).Namespace(ref.Namespace).
326
326
+
Watch(ctx, metav1.ListOptions{
327
327
+
FieldSelector: fields.OneTermEqualSelector(
328
328
+
"metadata.name", ref.PipelineRunName,
329
329
+
).String(),
330
330
+
})
331
331
+
if err != nil {
332
332
+
logger.Debug("watch PipelineRun status; falling back to polling", "err", err)
333
333
+
p.pollPipelineRun(ctx, ref, logger, last)
334
334
+
return
335
335
+
}
336
336
+
defer w.Stop()
337
337
+
338
338
+
for {
339
339
+
select {
340
340
+
case <-ctx.Done():
341
341
+
return
342
342
+
case ev, ok := <-w.ResultChan():
343
343
+
if !ok {
344
344
+
p.pollPipelineRun(ctx, ref, logger, last)
345
345
+
return
346
346
+
}
347
347
+
obj, ok := ev.Object.(*unstructured.Unstructured)
348
348
+
if !ok {
349
349
+
continue
350
350
+
}
351
351
+
status, terminal, ok := mapTektonPipelineRunStatus(obj)
352
352
+
if !ok || status == last {
353
353
+
if terminal {
354
354
+
return
355
355
+
}
356
356
+
continue
357
357
+
}
358
358
+
last = status
359
359
+
if err := p.publishStatus(ctx, ref.PipelineURI, ref.Workflow,
360
360
+
status, ref.PipelineRunName, nil, nil); err != nil {
361
361
+
logger.Error("publish tekton status", "err", err, "status", status)
362
362
+
continue
363
363
+
}
364
364
+
if terminal {
365
365
+
return
366
366
+
}
367
367
+
}
368
368
+
}
369
369
+
}
370
370
+
371
371
+
func (p *tektonProvider) pollPipelineRun(
372
372
+
ctx context.Context,
373
373
+
ref TektonRunRef,
374
374
+
logger *slog.Logger,
375
375
+
last string,
376
376
+
) {
377
377
+
ticker := time.NewTicker(5 * time.Second)
378
378
+
defer ticker.Stop()
379
379
+
for {
380
380
+
select {
381
381
+
case <-ctx.Done():
382
382
+
return
383
383
+
case <-ticker.C:
384
384
+
obj, err := p.dyn.Resource(pipelineRunsGVR).Namespace(ref.Namespace).
385
385
+
Get(ctx, ref.PipelineRunName, metav1.GetOptions{})
386
386
+
if apierrors.IsNotFound(err) {
387
387
+
logger.Warn("PipelineRun disappeared while watching")
388
388
+
return
389
389
+
}
390
390
+
if err != nil {
391
391
+
logger.Debug("get PipelineRun status", "err", err)
392
392
+
continue
393
393
+
}
394
394
+
status, terminal, ok := mapTektonPipelineRunStatus(obj)
395
395
+
if !ok || status == last {
396
396
+
if terminal {
397
397
+
return
398
398
+
}
399
399
+
continue
400
400
+
}
401
401
+
last = status
402
402
+
if err := p.publishStatus(ctx, ref.PipelineURI, ref.Workflow,
403
403
+
status, ref.PipelineRunName, nil, nil); err != nil {
404
404
+
logger.Error("publish tekton status", "err", err, "status", status)
405
405
+
continue
406
406
+
}
407
407
+
if terminal {
408
408
+
return
409
409
+
}
410
410
+
}
411
411
+
}
412
412
+
}
413
413
+
414
414
+
// mapTektonPipelineRunStatus translates Tekton's Succeeded condition
415
415
+
// into the Tangled status strings consumed by the appview.
416
416
+
func mapTektonPipelineRunStatus(obj *unstructured.Unstructured) (status string, terminal bool, ok bool) {
417
417
+
conditions, ok, _ := unstructured.NestedSlice(obj.Object, "status", "conditions")
418
418
+
if !ok || len(conditions) == 0 {
419
419
+
return "", false, false
420
420
+
}
421
421
+
for _, raw := range conditions {
422
422
+
cond, _ := raw.(map[string]interface{})
423
423
+
if cond["type"] != "Succeeded" {
424
424
+
continue
425
425
+
}
426
426
+
condStatus, _ := cond["status"].(string)
427
427
+
reason, _ := cond["reason"].(string)
428
428
+
switch condStatus {
429
429
+
case "True":
430
430
+
return "success", true, true
431
431
+
case "False":
432
432
+
if tektonReasonCancelled(reason) {
433
433
+
return "cancelled", true, true
434
434
+
}
435
435
+
return "failed", true, true
436
436
+
case "Unknown":
437
437
+
return "running", false, true
438
438
+
default:
439
439
+
return "", false, false
440
440
+
}
441
441
+
}
442
442
+
return "", false, false
443
443
+
}
444
444
+
445
445
+
func tektonReasonCancelled(reason string) bool {
446
446
+
r := strings.ToLower(reason)
447
447
+
return strings.Contains(r, "cancel") || strings.Contains(r, "stop")
448
448
+
}
449
449
+
450
450
+
func (p *tektonProvider) Logs(
451
451
+
ctx context.Context,
452
452
+
knot string,
453
453
+
pipelineRkey string,
454
454
+
workflow string,
455
455
+
) (<-chan LogLine, error) {
456
456
+
ref, err := p.st.LookupTektonRunByTuple(ctx, knot, pipelineRkey, workflow)
457
457
+
if err != nil {
458
458
+
return nil, fmt.Errorf("lookup tekton run mapping: %w", err)
459
459
+
}
460
460
+
if ref == nil {
461
461
+
return nil, ErrLogsNotFound
462
462
+
}
463
463
+
464
464
+
taskRuns, err := p.taskRunsForPipelineRun(ctx, *ref)
465
465
+
if err != nil {
466
466
+
return nil, err
467
467
+
}
468
468
+
if len(taskRuns) == 0 {
469
469
+
return nil, ErrLogsNotFound
470
470
+
}
471
471
+
472
472
+
out := make(chan LogLine, 32)
473
473
+
go func() {
474
474
+
defer close(out)
475
475
+
stepID := 0
476
476
+
for _, tr := range taskRuns {
477
477
+
taskName := tr.GetName()
478
478
+
if taskName == "" {
479
479
+
taskName = fmt.Sprintf("task %d", stepID)
480
480
+
}
481
481
+
if !sendLine(ctx, out, LogLine{
482
482
+
Kind: LogKindControl,
483
483
+
Time: time.Now(),
484
484
+
Content: taskName,
485
485
+
StepId: stepID,
486
486
+
StepStatus: StepStatusStart,
487
487
+
}) {
488
488
+
return
489
489
+
}
490
490
+
491
491
+
p.streamTaskRunLogs(ctx, out, *ref, tr, stepID)
492
492
+
493
493
+
if !sendLine(ctx, out, LogLine{
494
494
+
Kind: LogKindControl,
495
495
+
Time: time.Now(),
496
496
+
Content: taskName,
497
497
+
StepId: stepID,
498
498
+
StepStatus: StepStatusEnd,
499
499
+
}) {
500
500
+
return
501
501
+
}
502
502
+
stepID++
503
503
+
}
504
504
+
}()
505
505
+
return out, nil
506
506
+
}
507
507
+
508
508
+
func (p *tektonProvider) taskRunsForPipelineRun(ctx context.Context, ref TektonRunRef) ([]unstructured.Unstructured, error) {
509
509
+
sel := labels.Set{"tekton.dev/pipelineRun": ref.PipelineRunName}.String()
510
510
+
list, err := p.dyn.Resource(taskRunsGVR).Namespace(ref.Namespace).
511
511
+
List(ctx, metav1.ListOptions{LabelSelector: sel})
512
512
+
if err != nil {
513
513
+
return nil, fmt.Errorf("list Tekton TaskRuns: %w", err)
514
514
+
}
515
515
+
items := append([]unstructured.Unstructured(nil), list.Items...)
516
516
+
sort.Slice(items, func(i, j int) bool {
517
517
+
ti := items[i].GetCreationTimestamp()
518
518
+
tj := items[j].GetCreationTimestamp()
519
519
+
return ti.Before(&tj)
520
520
+
})
521
521
+
return items, nil
522
522
+
}
523
523
+
524
524
+
func (p *tektonProvider) streamTaskRunLogs(
525
525
+
ctx context.Context,
526
526
+
out chan<- LogLine,
527
527
+
ref TektonRunRef,
528
528
+
tr unstructured.Unstructured,
529
529
+
stepID int,
530
530
+
) {
531
531
+
pods, err := p.podsForTaskRun(ctx, ref.Namespace, tr.GetName())
532
532
+
if err != nil {
533
533
+
p.log.Debug("list pods for TaskRun", "err", err,
534
534
+
"task_run", tr.GetName(), "pipeline_run", ref.PipelineRunName,
535
535
+
)
536
536
+
return
537
537
+
}
538
538
+
for _, pod := range pods {
539
539
+
for _, c := range append(pod.Spec.InitContainers, pod.Spec.Containers...) {
540
540
+
req := p.kube.CoreV1().Pods(ref.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{
541
541
+
Container: c.Name,
542
542
+
})
543
543
+
rc, err := req.Stream(ctx)
544
544
+
if err != nil {
545
545
+
p.log.Debug("stream pod logs", "err", err,
546
546
+
"pod", pod.Name, "container", c.Name,
547
547
+
)
548
548
+
continue
549
549
+
}
550
550
+
p.sendReaderLines(ctx, out, rc, stepID)
551
551
+
_ = rc.Close()
552
552
+
}
553
553
+
}
554
554
+
}
555
555
+
556
556
+
func (p *tektonProvider) podsForTaskRun(ctx context.Context, namespace, taskRun string) ([]corev1.Pod, error) {
557
557
+
sel := labels.Set{"tekton.dev/taskRun": taskRun}.String()
558
558
+
list, err := p.kube.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
559
559
+
LabelSelector: sel,
560
560
+
})
561
561
+
if err != nil {
562
562
+
return nil, fmt.Errorf("list pods: %w", err)
563
563
+
}
564
564
+
pods := append([]corev1.Pod(nil), list.Items...)
565
565
+
sort.Slice(pods, func(i, j int) bool {
566
566
+
return pods[i].CreationTimestamp.Before(&pods[j].CreationTimestamp)
567
567
+
})
568
568
+
return pods, nil
569
569
+
}
570
570
+
571
571
+
func (p *tektonProvider) sendReaderLines(
572
572
+
ctx context.Context,
573
573
+
out chan<- LogLine,
574
574
+
rc io.Reader,
575
575
+
stepID int,
576
576
+
) {
577
577
+
scanner := bufio.NewScanner(rc)
578
578
+
for scanner.Scan() {
579
579
+
if !sendLine(ctx, out, LogLine{
580
580
+
Kind: LogKindData,
581
581
+
Time: time.Now(),
582
582
+
Content: scanner.Text() + "\n",
583
583
+
StepId: stepID,
584
584
+
Stream: "stdout",
585
585
+
}) {
586
586
+
return
587
587
+
}
588
588
+
}
589
589
+
if err := scanner.Err(); err != nil {
590
590
+
p.log.Debug("scan pod log", "err", err)
591
591
+
}
592
592
+
}
593
593
+
594
594
+
func (p *tektonProvider) publishStatus(
595
595
+
ctx context.Context,
596
596
+
pipelineURI, workflow, status, runName string,
597
597
+
errMsg *string,
598
598
+
exitCode *int64,
599
599
+
) error {
600
600
+
rec := tangled.PipelineStatus{
601
601
+
LexiconTypeID: tangled.PipelineStatusNSID,
602
602
+
Pipeline: pipelineURI,
603
603
+
Workflow: workflow,
604
604
+
Status: status,
605
605
+
CreatedAt: time.Now().UTC().Format(time.RFC3339),
606
606
+
Error: errMsg,
607
607
+
ExitCode: exitCode,
608
608
+
}
609
609
+
body, err := json.Marshal(rec)
610
610
+
if err != nil {
611
611
+
return fmt.Errorf("marshal pipeline.status: %w", err)
612
612
+
}
613
613
+
rkey := fmt.Sprintf("tk-%s-%s-%d", runName, status, time.Now().UnixNano())
614
614
+
if _, err := p.br.Publish(ctx, rkey, tangled.PipelineStatusNSID, body); err != nil {
615
615
+
return fmt.Errorf("publish pipeline.status: %w", err)
616
616
+
}
617
617
+
return nil
618
618
+
}
619
619
+
620
620
+
func tektonPipelineRunName(knot, pipelineRkey, workflow, commit, branch string) string {
621
621
+
h := sha256.Sum256([]byte(strings.Join(
622
622
+
[]string{knot, pipelineRkey, workflow, commit, branch}, "\x00",
623
623
+
)))
624
624
+
suffix := hex.EncodeToString(h[:])[:12]
625
625
+
base := dnsLabel("tack-" + workflow)
626
626
+
maxBase := 63 - len(suffix) - 1
627
627
+
if len(base) > maxBase {
628
628
+
base = strings.TrimRight(base[:maxBase], "-")
629
629
+
}
630
630
+
if base == "" {
631
631
+
base = "tack"
632
632
+
}
633
633
+
return base + "-" + suffix
634
634
+
}
635
635
+
636
636
+
func dnsLabel(s string) string {
637
637
+
var b strings.Builder
638
638
+
lastDash := false
639
639
+
for _, r := range strings.ToLower(s) {
640
640
+
ok := unicode.IsLetter(r) || unicode.IsDigit(r)
641
641
+
if ok {
642
642
+
b.WriteRune(r)
643
643
+
lastDash = false
644
644
+
continue
645
645
+
}
646
646
+
if !lastDash {
647
647
+
b.WriteByte('-')
648
648
+
lastDash = true
649
649
+
}
650
650
+
}
651
651
+
return strings.Trim(b.String(), "-")
652
652
+
}
653
653
+
654
654
+
func labelValue(s string) string {
655
655
+
v := dnsLabel(s)
656
656
+
if len(v) > 63 {
657
657
+
v = strings.TrimRight(v[:63], "-")
658
658
+
}
659
659
+
if v == "" {
660
660
+
return "unknown"
661
661
+
}
662
662
+
return v
663
663
+
}
···
1
1
+
package main
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"encoding/json"
6
6
+
"errors"
7
7
+
"log/slog"
8
8
+
"testing"
9
9
+
"time"
10
10
+
11
11
+
corev1 "k8s.io/api/core/v1"
12
12
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13
13
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14
14
+
"k8s.io/apimachinery/pkg/runtime"
15
15
+
runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
16
16
+
dynamicfake "k8s.io/client-go/dynamic/fake"
17
17
+
kubefake "k8s.io/client-go/kubernetes/fake"
18
18
+
"tangled.org/core/api/tangled"
19
19
+
)
20
20
+
21
21
+
func newTektonTestProvider(t *testing.T, objs ...runtime.Object) (*tektonProvider, *store, *broker) {
22
22
+
t.Helper()
23
23
+
st := newTestStore(t)
24
24
+
br := newBroker(st)
25
25
+
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
26
26
+
runtime.NewScheme(),
27
27
+
map[runtimeschema.GroupVersionResource]string{
28
28
+
pipelineRunsGVR: "PipelineRunList",
29
29
+
taskRunsGVR: "TaskRunList",
30
30
+
},
31
31
+
objs...,
32
32
+
)
33
33
+
kube := kubefake.NewSimpleClientset()
34
34
+
p := newTektonProvider(br, st, dyn, kube, "ci", slog.Default())
35
35
+
return p, st, br
36
36
+
}
37
37
+
38
38
+
func TestTektonWorkflowConfig(t *testing.T) {
39
39
+
raw := "tack:\n tekton:\n pipeline: repo-ci\n service_account: runner\n params:\n image: example/app\n"
40
40
+
cfg, err := parseTektonWorkflowConfig(raw)
41
41
+
if err != nil {
42
42
+
t.Fatalf("parse: %v", err)
43
43
+
}
44
44
+
if cfg.Pipeline != "repo-ci" || cfg.ServiceAccount != "runner" {
45
45
+
t.Fatalf("cfg mismatch: %+v", cfg)
46
46
+
}
47
47
+
if got := cfg.Params["image"]; got != "example/app" {
48
48
+
t.Fatalf("params[image] = %q", got)
49
49
+
}
50
50
+
51
51
+
if _, err := parseTektonWorkflowConfig("tack:\n tekton: {}\n"); err == nil {
52
52
+
t.Fatal("missing pipeline should fail")
53
53
+
}
54
54
+
}
55
55
+
56
56
+
func TestTektonBuildPipelineRun(t *testing.T) {
57
57
+
cfg := &tektonWorkflowConfig{
58
58
+
Pipeline: "repo-ci",
59
59
+
ServiceAccount: "runner",
60
60
+
Params: map[string]string{
61
61
+
"image": "example/app",
62
62
+
},
63
63
+
}
64
64
+
name := tektonPipelineRunName("knot.example.com", "rkey-1", "ci.yml", "abcdef", "main")
65
65
+
if len(name) > 63 || name == "" {
66
66
+
t.Fatalf("bad generated name: %q", name)
67
67
+
}
68
68
+
69
69
+
obj := buildTektonPipelineRun("ci", name, cfg,
70
70
+
"knot.example.com", "rkey-1", "did:plc:actor", "abcdef", "main",
71
71
+
&tangled.Pipeline_Workflow{Name: "ci.yml"},
72
72
+
)
73
73
+
if obj.GetAPIVersion() != tektonAPIVersion || obj.GetKind() != tektonRunKind {
74
74
+
t.Fatalf("type meta mismatch: %s %s", obj.GetAPIVersion(), obj.GetKind())
75
75
+
}
76
76
+
pipeline, _, _ := unstructured.NestedString(obj.Object, "spec", "pipelineRef", "name")
77
77
+
if pipeline != "repo-ci" {
78
78
+
t.Fatalf("pipelineRef.name = %q", pipeline)
79
79
+
}
80
80
+
sa, _, _ := unstructured.NestedString(obj.Object, "spec", "serviceAccountName")
81
81
+
if sa != "runner" {
82
82
+
t.Fatalf("serviceAccountName = %q", sa)
83
83
+
}
84
84
+
params, _, _ := unstructured.NestedSlice(obj.Object, "spec", "params")
85
85
+
if len(params) != 1 {
86
86
+
t.Fatalf("params = %+v", params)
87
87
+
}
88
88
+
if obj.GetAnnotations()[tektonAnnotationActor] != "did:plc:actor" ||
89
89
+
obj.GetAnnotations()[tektonAnnotationCommit] != "abcdef" {
90
90
+
t.Fatalf("annotations missing identity: %+v", obj.GetAnnotations())
91
91
+
}
92
92
+
}
93
93
+
94
94
+
func TestTektonStatusMapping(t *testing.T) {
95
95
+
tests := []struct {
96
96
+
name string
97
97
+
cond string
98
98
+
reason string
99
99
+
status string
100
100
+
terminal bool
101
101
+
ok bool
102
102
+
}{
103
103
+
{name: "unknown", cond: "Unknown", status: "running", ok: true},
104
104
+
{name: "success", cond: "True", status: "success", terminal: true, ok: true},
105
105
+
{name: "failed", cond: "False", reason: "Failed", status: "failed", terminal: true, ok: true},
106
106
+
{name: "cancelled", cond: "False", reason: "PipelineRunCancelled", status: "cancelled", terminal: true, ok: true},
107
107
+
{name: "stopped", cond: "False", reason: "PipelineRunStopped", status: "cancelled", terminal: true, ok: true},
108
108
+
}
109
109
+
for _, tt := range tests {
110
110
+
t.Run(tt.name, func(t *testing.T) {
111
111
+
obj := tektonStatusObject(tt.cond, tt.reason)
112
112
+
status, terminal, ok := mapTektonPipelineRunStatus(obj)
113
113
+
if status != tt.status || terminal != tt.terminal || ok != tt.ok {
114
114
+
t.Fatalf("got %q/%v/%v; want %q/%v/%v",
115
115
+
status, terminal, ok, tt.status, tt.terminal, tt.ok)
116
116
+
}
117
117
+
})
118
118
+
}
119
119
+
}
120
120
+
121
121
+
func TestTektonSpawnCreatesPipelineRun(t *testing.T) {
122
122
+
p, st, _ := newTektonTestProvider(t)
123
123
+
ctx, cancel := context.WithCancel(context.Background())
124
124
+
defer cancel()
125
125
+
126
126
+
trigger := &tangled.Pipeline_TriggerMetadata{
127
127
+
Push: &tangled.Pipeline_PushTriggerData{
128
128
+
NewSha: "abcdef0123",
129
129
+
Ref: "refs/heads/main",
130
130
+
},
131
131
+
}
132
132
+
p.Spawn(ctx, "knot.example.com", "rkey-1", "did:plc:actor", trigger,
133
133
+
[]*tangled.Pipeline_Workflow{{Name: "ci.yml",
134
134
+
Raw: "tack:\n tekton:\n pipeline: repo-ci\n"}},
135
135
+
)
136
136
+
137
137
+
ref := waitTektonRef(t, st, "knot.example.com", "rkey-1", "ci.yml")
138
138
+
if ref.Namespace != "ci" || ref.PipelineName != "repo-ci" {
139
139
+
t.Fatalf("ref mismatch: %+v", ref)
140
140
+
}
141
141
+
142
142
+
obj, err := p.dyn.Resource(pipelineRunsGVR).Namespace("ci").
143
143
+
Get(context.Background(), ref.PipelineRunName, metav1.GetOptions{})
144
144
+
if err != nil {
145
145
+
t.Fatalf("get PipelineRun: %v", err)
146
146
+
}
147
147
+
pipeline, _, _ := unstructured.NestedString(obj.Object, "spec", "pipelineRef", "name")
148
148
+
if pipeline != "repo-ci" {
149
149
+
t.Fatalf("pipelineRef.name = %q", pipeline)
150
150
+
}
151
151
+
152
152
+
rows, err := st.EventsAfter(context.Background(), 0)
153
153
+
if err != nil {
154
154
+
t.Fatalf("EventsAfter: %v", err)
155
155
+
}
156
156
+
if len(rows) != 1 {
157
157
+
t.Fatalf("got %d events, want 1", len(rows))
158
158
+
}
159
159
+
var rec tangled.PipelineStatus
160
160
+
if err := json.Unmarshal(rows[0].EventJSON, &rec); err != nil {
161
161
+
t.Fatalf("decode status: %v", err)
162
162
+
}
163
163
+
if rec.Status != "pending" || rec.Workflow != "ci.yml" {
164
164
+
t.Fatalf("bad pending status: %+v", rec)
165
165
+
}
166
166
+
}
167
167
+
168
168
+
func TestTektonSpawnAlreadyExists(t *testing.T) {
169
169
+
name := tektonPipelineRunName("knot.example.com", "rkey-1", "ci.yml", "abcdef0123", "main")
170
170
+
existing := buildTektonPipelineRun("ci", name,
171
171
+
&tektonWorkflowConfig{Pipeline: "repo-ci"},
172
172
+
"knot.example.com", "rkey-1", "did:plc:actor", "abcdef0123", "main",
173
173
+
&tangled.Pipeline_Workflow{Name: "ci.yml"},
174
174
+
)
175
175
+
existing.SetUID("uid-1")
176
176
+
p, st, _ := newTektonTestProvider(t, existing)
177
177
+
ctx, cancel := context.WithCancel(context.Background())
178
178
+
defer cancel()
179
179
+
180
180
+
p.Spawn(ctx, "knot.example.com", "rkey-1", "did:plc:actor",
181
181
+
&tangled.Pipeline_TriggerMetadata{Push: &tangled.Pipeline_PushTriggerData{
182
182
+
NewSha: "abcdef0123",
183
183
+
Ref: "refs/heads/main",
184
184
+
}},
185
185
+
[]*tangled.Pipeline_Workflow{{Name: "ci.yml",
186
186
+
Raw: "tack:\n tekton:\n pipeline: repo-ci\n"}},
187
187
+
)
188
188
+
189
189
+
ref := waitTektonRef(t, st, "knot.example.com", "rkey-1", "ci.yml")
190
190
+
if ref.PipelineRunName != name || ref.PipelineRunUID != "uid-1" {
191
191
+
t.Fatalf("ref mismatch: %+v", ref)
192
192
+
}
193
193
+
}
194
194
+
195
195
+
func TestTektonLogsLookup(t *testing.T) {
196
196
+
p, st, _ := newTektonTestProvider(t)
197
197
+
ctx := context.Background()
198
198
+
if _, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml"); !errors.Is(err, ErrLogsNotFound) {
199
199
+
t.Fatalf("logs before mapping err = %v; want ErrLogsNotFound", err)
200
200
+
}
201
201
+
ref := TektonRunRef{
202
202
+
Knot: "knot.example.com",
203
203
+
PipelineRkey: "rkey-1",
204
204
+
Workflow: "ci.yml",
205
205
+
Namespace: "ci",
206
206
+
PipelineRunName: "run-1",
207
207
+
PipelineRunUID: "uid-1",
208
208
+
PipelineName: "repo-ci",
209
209
+
PipelineURI: pipelineATURI("knot.example.com", "rkey-1"),
210
210
+
}
211
211
+
if err := st.InsertTektonRun(ctx, ref); err != nil {
212
212
+
t.Fatalf("insert ref: %v", err)
213
213
+
}
214
214
+
if _, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml"); !errors.Is(err, ErrLogsNotFound) {
215
215
+
t.Fatalf("logs before TaskRuns err = %v; want ErrLogsNotFound", err)
216
216
+
}
217
217
+
218
218
+
taskRun := &unstructured.Unstructured{Object: map[string]interface{}{
219
219
+
"apiVersion": "tekton.dev/v1",
220
220
+
"kind": "TaskRun",
221
221
+
"metadata": map[string]interface{}{
222
222
+
"name": "task-1",
223
223
+
"namespace": "ci",
224
224
+
"labels": map[string]interface{}{
225
225
+
"tekton.dev/pipelineRun": "run-1",
226
226
+
},
227
227
+
},
228
228
+
}}
229
229
+
_, err := p.dyn.Resource(taskRunsGVR).Namespace("ci").
230
230
+
Create(ctx, taskRun, metav1.CreateOptions{})
231
231
+
if err != nil {
232
232
+
t.Fatalf("create TaskRun: %v", err)
233
233
+
}
234
234
+
_, err = p.kube.CoreV1().Pods("ci").Create(ctx, &corev1.Pod{
235
235
+
ObjectMeta: metav1.ObjectMeta{
236
236
+
Name: "pod-1",
237
237
+
Namespace: "ci",
238
238
+
Labels: map[string]string{
239
239
+
"tekton.dev/taskRun": "task-1",
240
240
+
},
241
241
+
},
242
242
+
Spec: corev1.PodSpec{
243
243
+
Containers: []corev1.Container{{Name: "step-test", Image: "busybox"}},
244
244
+
},
245
245
+
}, metav1.CreateOptions{})
246
246
+
if err != nil {
247
247
+
t.Fatalf("create pod: %v", err)
248
248
+
}
249
249
+
250
250
+
ch, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml")
251
251
+
if err != nil {
252
252
+
t.Fatalf("Logs after pods: %v", err)
253
253
+
}
254
254
+
var got []LogLine
255
255
+
for line := range ch {
256
256
+
got = append(got, line)
257
257
+
}
258
258
+
if len(got) < 2 || got[0].StepStatus != StepStatusStart ||
259
259
+
got[len(got)-1].StepStatus != StepStatusEnd {
260
260
+
t.Fatalf("log frames = %+v", got)
261
261
+
}
262
262
+
}
263
263
+
264
264
+
func tektonStatusObject(condStatus, reason string) *unstructured.Unstructured {
265
265
+
return &unstructured.Unstructured{Object: map[string]interface{}{
266
266
+
"status": map[string]interface{}{
267
267
+
"conditions": []interface{}{map[string]interface{}{
268
268
+
"type": "Succeeded",
269
269
+
"status": condStatus,
270
270
+
"reason": reason,
271
271
+
}},
272
272
+
},
273
273
+
}}
274
274
+
}
275
275
+
276
276
+
func waitTektonRef(t *testing.T, st *store, knot, rkey, workflow string) *TektonRunRef {
277
277
+
t.Helper()
278
278
+
deadline := time.Now().Add(2 * time.Second)
279
279
+
for time.Now().Before(deadline) {
280
280
+
ref, err := st.LookupTektonRunByTuple(context.Background(), knot, rkey, workflow)
281
281
+
if err != nil {
282
282
+
t.Fatalf("lookup: %v", err)
283
283
+
}
284
284
+
if ref != nil {
285
285
+
return ref
286
286
+
}
287
287
+
time.Sleep(20 * time.Millisecond)
288
288
+
}
289
289
+
t.Fatal("tekton run row not persisted within deadline")
290
290
+
return nil
291
291
+
}
···
526
526
PipelineURI string
527
527
}
528
528
529
529
+
// TektonRunRef is the persisted link from a Tangled workflow tuple
530
530
+
// to the in-cluster PipelineRun tack created for it. The tuple is the
531
531
+
// user-facing identity the appview knows; namespace/name/uid are the
532
532
+
// Kubernetes identity needed for status watching and log lookup.
533
533
+
type TektonRunRef struct {
534
534
+
Knot string
535
535
+
PipelineRkey string
536
536
+
Workflow string
537
537
+
Namespace string
538
538
+
PipelineRunName string
539
539
+
PipelineRunUID string
540
540
+
PipelineName string
541
541
+
PipelineURI string
542
542
+
}
543
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
642
+
}
643
643
+
return &ref, nil
644
644
+
}
645
645
+
646
646
+
// InsertTektonRun records the latest PipelineRun created for a Tangled
647
647
+
// workflow tuple. Reusing the tuple as the primary key intentionally
648
648
+
// makes /logs resolve to the newest run for that workflow identity.
649
649
+
func (s *store) InsertTektonRun(ctx context.Context, ref TektonRunRef) error {
650
650
+
now := time.Now().UTC()
651
651
+
_, err := s.db.ExecContext(ctx,
652
652
+
`INSERT INTO tekton_runs (
653
653
+
knot, pipeline_rkey, workflow,
654
654
+
namespace, pipeline_run_name, pipeline_run_uid,
655
655
+
pipeline_name, pipeline_uri, created_at, created_unix_ns
656
656
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
657
657
+
ON CONFLICT(knot, pipeline_rkey, workflow) DO UPDATE SET
658
658
+
namespace = excluded.namespace,
659
659
+
pipeline_run_name = excluded.pipeline_run_name,
660
660
+
pipeline_run_uid = excluded.pipeline_run_uid,
661
661
+
pipeline_name = excluded.pipeline_name,
662
662
+
pipeline_uri = excluded.pipeline_uri,
663
663
+
created_at = excluded.created_at,
664
664
+
created_unix_ns = excluded.created_unix_ns`,
665
665
+
ref.Knot, ref.PipelineRkey, ref.Workflow,
666
666
+
ref.Namespace, ref.PipelineRunName, ref.PipelineRunUID,
667
667
+
ref.PipelineName, ref.PipelineURI, now.Format(time.RFC3339Nano), now.UnixNano(),
668
668
+
)
669
669
+
if err != nil {
670
670
+
return fmt.Errorf("insert tekton_run: %w", err)
671
671
+
}
672
672
+
return nil
673
673
+
}
674
674
+
675
675
+
// LookupTektonRunByTuple resolves the appview's path-based identity to
676
676
+
// the concrete PipelineRun tack created in Kubernetes.
677
677
+
func (s *store) LookupTektonRunByTuple(ctx context.Context, knot, pipelineRkey, workflow string) (*TektonRunRef, error) {
678
678
+
var ref TektonRunRef
679
679
+
err := s.db.QueryRowContext(ctx,
680
680
+
`SELECT knot, pipeline_rkey, workflow,
681
681
+
namespace, pipeline_run_name, pipeline_run_uid,
682
682
+
pipeline_name, pipeline_uri
683
683
+
FROM tekton_runs
684
684
+
WHERE knot = ? AND pipeline_rkey = ? AND workflow = ?`,
685
685
+
knot, pipelineRkey, workflow,
686
686
+
).Scan(
687
687
+
&ref.Knot, &ref.PipelineRkey, &ref.Workflow,
688
688
+
&ref.Namespace, &ref.PipelineRunName, &ref.PipelineRunUID,
689
689
+
&ref.PipelineName, &ref.PipelineURI,
690
690
+
)
691
691
+
if errors.Is(err, sql.ErrNoRows) {
692
692
+
return nil, nil
693
693
+
}
694
694
+
if err != nil {
695
695
+
return nil, fmt.Errorf("lookup tekton_run by tuple: %w", err)
627
696
}
628
697
return &ref, nil
629
698
}
···
121
121
);
122
122
CREATE INDEX IF NOT EXISTS buildkite_builds_lookup
123
123
ON buildkite_builds (knot, pipeline_rkey, workflow);
124
124
+
125
125
+
-- Mapping from a Tangled workflow tuple to the latest Tekton
126
126
+
-- PipelineRun tack created for it. Unlike Buildkite webhooks, Tekton
127
127
+
-- status observation happens in-process, so the primary read path is
128
128
+
-- /logs resolving (knot, pipeline_rkey, workflow) back to the concrete
129
129
+
-- PipelineRun whose TaskRuns and pods hold output.
130
130
+
CREATE TABLE IF NOT EXISTS tekton_runs (
131
131
+
knot TEXT NOT NULL,
132
132
+
pipeline_rkey TEXT NOT NULL,
133
133
+
workflow TEXT NOT NULL,
134
134
+
namespace TEXT NOT NULL,
135
135
+
pipeline_run_name TEXT NOT NULL,
136
136
+
pipeline_run_uid TEXT NOT NULL,
137
137
+
pipeline_name TEXT NOT NULL,
138
138
+
pipeline_uri TEXT NOT NULL,
139
139
+
created_at TEXT NOT NULL,
140
140
+
created_unix_ns INTEGER NOT NULL,
141
141
+
PRIMARY KEY (knot, pipeline_rkey, workflow)
142
142
+
);
143
143
+
CREATE INDEX IF NOT EXISTS tekton_runs_uid
144
144
+
ON tekton_runs (pipeline_run_uid);
124
145
`
125
146
126
147
// migrate applies the schema. Safe to call repeatedly.