Stitch any CI into Tangled
1package k8s
2
3// Package k8s exposes the tiny slice of the Kubernetes API tack's Tekton
4// provider actually needs. The goal is to keep Tekton support in-process
5// without dragging the full client-go stack into the module graph.
6
7import (
8 "context"
9 "errors"
10 "io"
11 "time"
12)
13
14// ErrNotFound mirrors the API server's 404 in a way callers can branch on with
15// errors.Is instead of inspecting status codes or response bodies.
16var ErrNotFound = errors.New("k8s: not found")
17
18// ErrAlreadyExists mirrors the API server's 409 for create calls whose name is
19// already present.
20var ErrAlreadyExists = errors.New("k8s: already exists")
21
22// Client is the minimal Kubernetes surface tack needs today: CRUD-ish access to
23// arbitrary JSON objects, plus pod listing and log streaming.
24type Client interface {
25 CreateObject(
26 ctx context.Context,
27 gvr GVR,
28 namespace string,
29 obj Object,
30 ) (Object, error)
31
32 GetObject(
33 ctx context.Context,
34 gvr GVR,
35 namespace string,
36 name string,
37 ) (Object, error)
38
39 ListObjects(
40 ctx context.Context,
41 gvr GVR,
42 namespace string,
43 opts ListOptions,
44 ) ([]Object, error)
45
46 WatchObjects(
47 ctx context.Context,
48 gvr GVR,
49 namespace string,
50 opts ListOptions,
51 ) (WatchInterface, error)
52
53 ListPods(
54 ctx context.Context,
55 namespace string,
56 labelSelector string,
57 ) ([]Pod, error)
58
59 StreamPodLogs(
60 ctx context.Context,
61 namespace string,
62 podName string,
63 container string,
64 opts LogOptions,
65 ) (io.ReadCloser, error)
66}
67
68// LogOptions controls how StreamPodLogs reads container logs.
69type LogOptions struct {
70 // Follow tells the API server to keep the connection open and stream
71 // new log bytes as the container writes them, only EOFing when the
72 // container terminates (or ctx is cancelled). Without Follow, the
73 // returned reader is a snapshot: it yields the bytes that exist at
74 // request time and then EOFs immediately, even if the container is
75 // still running. Live-streaming callers MUST set Follow=true; using
76 // the snapshot mode for a still-running container makes the caller
77 // look at a frozen view and treat the container as if it had finished.
78 Follow bool
79}
80
81// GVR identifies a Kubernetes resource by the path segments the API server
82// routes on.
83type GVR struct {
84 Group string
85 Version string
86 Resource string
87}
88
89// ListOptions is the subset of query options tack currently uses.
90type ListOptions struct {
91 LabelSelector string
92 FieldSelector string
93}
94
95// WatchInterface matches the shape provider code already expects from a watch:
96// a receive-only event channel and an explicit stop hook.
97type WatchInterface interface {
98 ResultChan() <-chan WatchEvent
99 Stop()
100}
101
102// WatchEvent mirrors the API server's streaming watch envelope.
103type WatchEvent struct {
104 Type string
105 Object Object
106}
107
108// Object is an arbitrary Kubernetes resource decoded from JSON.
109type Object map[string]any
110
111// DeepCopy returns a recursive copy so callers can safely mutate the returned
112// object without aliasing shared test fixtures or cached state.
113func (o Object) DeepCopy() Object {
114 if o == nil {
115 return nil
116 }
117 return Object(deepCopyMap(map[string]any(o)))
118}
119
120// GetName returns metadata.name, or the empty string when absent.
121func (o Object) GetName() string {
122 v, _ := NestedString(map[string]any(o), "metadata", "name")
123 return v
124}
125
126// GetNamespace returns metadata.namespace, or the empty string when absent.
127func (o Object) GetNamespace() string {
128 v, _ := NestedString(map[string]any(o), "metadata", "namespace")
129 return v
130}
131
132// GetAPIVersion returns apiVersion, or the empty string when absent.
133func (o Object) GetAPIVersion() string {
134 v, _ := NestedString(map[string]any(o), "apiVersion")
135 return v
136}
137
138// GetKind returns kind, or the empty string when absent.
139func (o Object) GetKind() string {
140 v, _ := NestedString(map[string]any(o), "kind")
141 return v
142}
143
144// GetUID returns metadata.uid, or the empty string when absent.
145func (o Object) GetUID() string {
146 v, _ := NestedString(map[string]any(o), "metadata", "uid")
147 return v
148}
149
150// SetUID writes metadata.uid, creating metadata as needed.
151func (o Object) SetUID(uid string) {
152 meta := ensureNestedMap(o, "metadata")
153 meta["uid"] = uid
154}
155
156// SetCreationTimestamp writes metadata.creationTimestamp in UTC RFC3339 form.
157func (o Object) SetCreationTimestamp(ts time.Time) {
158 meta := ensureNestedMap(o, "metadata")
159 meta["creationTimestamp"] = ts.UTC().Format(time.RFC3339Nano)
160}
161
162// GetCreationTimestamp returns metadata.creationTimestamp, or the zero time when
163// the field is missing or malformed.
164func (o Object) GetCreationTimestamp() time.Time {
165 raw, ok := NestedString(map[string]any(o), "metadata", "creationTimestamp")
166 if !ok || raw == "" {
167 return time.Time{}
168 }
169 for _, layout := range []string{time.RFC3339Nano, time.RFC3339} {
170 if ts, err := time.Parse(layout, raw); err == nil {
171 return ts
172 }
173 }
174 return time.Time{}
175}
176
177// GetLabels returns metadata.labels as a string map. Non-string label values are
178// ignored because Kubernetes labels are strings on the wire.
179func (o Object) GetLabels() map[string]string {
180 raw, ok := NestedMap(map[string]any(o), "metadata", "labels")
181 if !ok {
182 return nil
183 }
184 out := make(map[string]string, len(raw))
185 for key, value := range raw {
186 if s, ok := value.(string); ok {
187 out[key] = s
188 }
189 }
190 return out
191}
192
193// GetAnnotations returns metadata.annotations as a string map.
194func (o Object) GetAnnotations() map[string]string {
195 raw, ok := NestedMap(map[string]any(o), "metadata", "annotations")
196 if !ok {
197 return nil
198 }
199 out := make(map[string]string, len(raw))
200 for key, value := range raw {
201 if s, ok := value.(string); ok {
202 out[key] = s
203 }
204 }
205 return out
206}
207
208// NestedString returns a nested string field from the object.
209func (o Object) NestedString(fields ...string) (string, bool) {
210 return NestedString(map[string]any(o), fields...)
211}
212
213// NestedSlice returns a nested slice field from the object.
214func (o Object) NestedSlice(fields ...string) ([]any, bool) {
215 return NestedSlice(map[string]any(o), fields...)
216}
217
218// NestedMap returns a nested map field from the object.
219func (o Object) NestedMap(fields ...string) (map[string]any, bool) {
220 return NestedMap(map[string]any(o), fields...)
221}
222
223// NestedString returns a nested string field from obj.
224func NestedString(obj map[string]any, fields ...string) (string, bool) {
225 value, ok := nestedValue(obj, fields...)
226 if !ok {
227 return "", false
228 }
229 s, ok := value.(string)
230 return s, ok
231}
232
233// NestedSlice returns a nested slice field from obj.
234func NestedSlice(obj map[string]any, fields ...string) ([]any, bool) {
235 value, ok := nestedValue(obj, fields...)
236 if !ok {
237 return nil, false
238 }
239 slice, ok := value.([]any)
240 return slice, ok
241}
242
243// NestedMap returns a nested map field from obj.
244func NestedMap(obj map[string]any, fields ...string) (map[string]any, bool) {
245 value, ok := nestedValue(obj, fields...)
246 if !ok {
247 return nil, false
248 }
249 m, ok := value.(map[string]any)
250 return m, ok
251}
252
253// Container is the small slice of a pod container spec tack cares about for
254// log streaming.
255type Container struct {
256 Name string
257}
258
259// Pod is the reduced pod shape tack uses to order containers and request logs.
260type Pod struct {
261 Name string
262 Namespace string
263 UID string
264 Labels map[string]string
265 CreationTimestamp time.Time
266 InitContainers []Container
267 Containers []Container
268}
269
270func nestedValue(obj map[string]any, fields ...string) (any, bool) {
271 current := any(obj)
272 for _, field := range fields {
273 m, ok := current.(map[string]any)
274 if !ok {
275 return nil, false
276 }
277 current, ok = m[field]
278 if !ok {
279 return nil, false
280 }
281 }
282 return current, true
283}
284
285func ensureNestedMap(obj map[string]any, fields ...string) map[string]any {
286 current := obj
287 for _, field := range fields {
288 next, ok := current[field].(map[string]any)
289 if !ok {
290 next = map[string]any{}
291 current[field] = next
292 }
293 current = next
294 }
295 return current
296}
297
298func deepCopyMap(in map[string]any) map[string]any {
299 out := make(map[string]any, len(in))
300 for key, value := range in {
301 out[key] = deepCopyValue(value)
302 }
303 return out
304}
305
306func deepCopySlice(in []any) []any {
307 out := make([]any, len(in))
308 for i, value := range in {
309 out[i] = deepCopyValue(value)
310 }
311 return out
312}
313
314func deepCopyValue(value any) any {
315 switch v := value.(type) {
316 case map[string]any:
317 return deepCopyMap(v)
318 case []any:
319 return deepCopySlice(v)
320 default:
321 return v
322 }
323}