Skip to content

Commit bddfca1

Browse files
committed
feat(binding): add binding-owned spritz lifecycle
1 parent 9224ca2 commit bddfca1

11 files changed

Lines changed: 3916 additions & 15 deletions

api/internal_bindings.go

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/sha256"
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"strings"
11+
"time"
12+
13+
"github.com/labstack/echo/v4"
14+
apierrors "k8s.io/apimachinery/pkg/api/errors"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"sigs.k8s.io/controller-runtime/pkg/client"
17+
18+
spritzv1 "spritz.sh/operator/api/v1"
19+
)
20+
21+
type internalBindingRequest struct {
22+
DesiredRevision string `json:"desiredRevision,omitempty"`
23+
Disconnected bool `json:"disconnected,omitempty"`
24+
Attributes map[string]string `json:"attributes,omitempty"`
25+
Principal internalCreatePrincipal `json:"principal"`
26+
Request json.RawMessage `json:"request"`
27+
AdoptActive *internalBindingInstanceRef `json:"adoptActive,omitempty"`
28+
AdoptedRevision string `json:"adoptedRevision,omitempty"`
29+
}
30+
31+
type internalBindingInstanceRef struct {
32+
Namespace string `json:"namespace,omitempty"`
33+
InstanceID string `json:"instanceId,omitempty"`
34+
Revision string `json:"revision,omitempty"`
35+
}
36+
37+
type internalBindingMetadata struct {
38+
Name string `json:"name"`
39+
Namespace string `json:"namespace"`
40+
CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"`
41+
}
42+
43+
type internalBindingTemplateSummary struct {
44+
PresetID string `json:"presetId,omitempty"`
45+
Source string `json:"source,omitempty"`
46+
RequestID string `json:"requestId,omitempty"`
47+
OwnerID string `json:"ownerId,omitempty"`
48+
}
49+
50+
type internalBindingSpecSummary struct {
51+
BindingKey string `json:"bindingKey"`
52+
DesiredRevision string `json:"desiredRevision,omitempty"`
53+
Disconnected bool `json:"disconnected,omitempty"`
54+
Attributes map[string]string `json:"attributes,omitempty"`
55+
Template internalBindingTemplateSummary `json:"template"`
56+
}
57+
58+
type internalBindingSummary struct {
59+
Metadata internalBindingMetadata `json:"metadata"`
60+
Spec internalBindingSpecSummary `json:"spec"`
61+
Status spritzv1.SpritzBindingStatus `json:"status"`
62+
}
63+
64+
func bindingResourceNameForKey(bindingKey string) string {
65+
normalized := strings.TrimSpace(bindingKey)
66+
sum := sha256.Sum256([]byte(normalized))
67+
prefix := sanitizeSpritzNameToken(normalized)
68+
if prefix == "" {
69+
prefix = "binding"
70+
}
71+
if len(prefix) > 36 {
72+
prefix = prefix[:36]
73+
prefix = strings.TrimRight(prefix, "-")
74+
}
75+
return fmt.Sprintf("%s-%x", prefix, sum[:8])
76+
}
77+
78+
func summarizeInternalBinding(binding *spritzv1.SpritzBinding) internalBindingSummary {
79+
return internalBindingSummary{
80+
Metadata: internalBindingMetadata{
81+
Name: binding.Name,
82+
Namespace: binding.Namespace,
83+
CreationTimestamp: binding.CreationTimestamp,
84+
},
85+
Spec: internalBindingSpecSummary{
86+
BindingKey: strings.TrimSpace(binding.Spec.BindingKey),
87+
DesiredRevision: strings.TrimSpace(binding.Spec.DesiredRevision),
88+
Disconnected: binding.Spec.Disconnected,
89+
Attributes: cloneStringMap(binding.Spec.Attributes),
90+
Template: internalBindingTemplateSummary{
91+
PresetID: strings.TrimSpace(binding.Spec.Template.PresetID),
92+
Source: strings.TrimSpace(binding.Spec.Template.Source),
93+
RequestID: strings.TrimSpace(binding.Spec.Template.RequestID),
94+
OwnerID: strings.TrimSpace(binding.Spec.Template.Spec.Owner.ID),
95+
},
96+
},
97+
Status: binding.Status,
98+
}
99+
}
100+
101+
func (s *server) getInternalBinding(c echo.Context) error {
102+
namespace, bindingName, err := s.resolveBindingPath(c)
103+
if err != nil {
104+
return writeError(c, http.StatusBadRequest, err.Error())
105+
}
106+
var binding spritzv1.SpritzBinding
107+
if err := s.client.Get(c.Request().Context(), client.ObjectKey{Namespace: namespace, Name: bindingName}, &binding); err != nil {
108+
if apierrors.IsNotFound(err) {
109+
return writeError(c, http.StatusNotFound, "not found")
110+
}
111+
return writeError(c, http.StatusInternalServerError, err.Error())
112+
}
113+
return writeJSON(c, http.StatusOK, summarizeInternalBinding(&binding))
114+
}
115+
116+
func (s *server) upsertInternalBinding(c echo.Context) error {
117+
namespace, bindingName, err := s.resolveBindingPath(c)
118+
if err != nil {
119+
return writeError(c, http.StatusBadRequest, err.Error())
120+
}
121+
122+
var body internalBindingRequest
123+
if err := c.Bind(&body); err != nil {
124+
return writeError(c, http.StatusBadRequest, "invalid json")
125+
}
126+
internalPrincipal, err := body.Principal.normalize()
127+
if err != nil {
128+
return writeError(c, http.StatusBadRequest, err.Error())
129+
}
130+
131+
var requestBody createRequest
132+
if err := json.Unmarshal(bytes.TrimSpace(body.Request), &requestBody); err != nil {
133+
return writeError(c, http.StatusBadRequest, "request is invalid")
134+
}
135+
if strings.TrimSpace(requestBody.Name) != "" {
136+
return writeError(c, http.StatusBadRequest, "request.name is not allowed for bindings")
137+
}
138+
139+
normalized, err := s.normalizeCreateRequest(c.Request().Context(), internalPrincipal, requestBody, false)
140+
if err != nil {
141+
return writeCreateRequestError(c, err)
142+
}
143+
requestBody = normalized.body
144+
145+
if err := s.ensureServiceAccount(c.Request().Context(), namespace, requestBody.Spec.ServiceAccountName); err != nil {
146+
return writeError(c, http.StatusInternalServerError, "failed to ensure service account")
147+
}
148+
149+
labels := map[string]string{
150+
ownerLabelKey: ownerLabelValue(requestBody.Spec.Owner.ID),
151+
actorLabelKey: actorLabelValue(internalPrincipal.ID),
152+
}
153+
if strings.TrimSpace(requestBody.PresetID) != "" {
154+
labels[presetLabelKey] = strings.TrimSpace(requestBody.PresetID)
155+
}
156+
157+
annotations := cloneStringMap(s.defaultMetadata)
158+
if annotations == nil {
159+
annotations = map[string]string{}
160+
}
161+
if strings.TrimSpace(requestBody.PresetID) != "" {
162+
annotations[presetIDAnnotationKey] = strings.TrimSpace(requestBody.PresetID)
163+
}
164+
165+
applySSHDefaults(&requestBody.Spec, s.sshDefaults, namespace)
166+
template := spritzv1.SpritzBindingTemplate{
167+
PresetID: strings.TrimSpace(requestBody.PresetID),
168+
NamePrefix: s.resolvedCreateNamePrefix(requestBody, normalized.requestedNamePrefix),
169+
Source: provisionerSource(&requestBody),
170+
RequestID: strings.TrimSpace(requestBody.RequestID),
171+
Spec: requestBody.Spec,
172+
Labels: labels,
173+
Annotations: annotations,
174+
}
175+
176+
binding := &spritzv1.SpritzBinding{}
177+
createNew := false
178+
if err := s.client.Get(c.Request().Context(), client.ObjectKey{Namespace: namespace, Name: bindingName}, binding); err != nil {
179+
if !apierrors.IsNotFound(err) {
180+
return writeError(c, http.StatusInternalServerError, err.Error())
181+
}
182+
createNew = true
183+
binding = &spritzv1.SpritzBinding{
184+
TypeMeta: metav1.TypeMeta{Kind: "SpritzBinding", APIVersion: spritzv1.GroupVersion.String()},
185+
ObjectMeta: metav1.ObjectMeta{
186+
Name: bindingName,
187+
Namespace: namespace,
188+
},
189+
}
190+
}
191+
192+
binding.Spec = spritzv1.SpritzBindingSpec{
193+
BindingKey: strings.TrimSpace(c.Param("bindingKey")),
194+
DesiredRevision: strings.TrimSpace(body.DesiredRevision),
195+
Disconnected: body.Disconnected,
196+
Attributes: cloneStringMap(body.Attributes),
197+
Template: template,
198+
AdoptActive: convertInternalBindingRef(body.AdoptActive, namespace),
199+
AdoptedRevision: strings.TrimSpace(body.AdoptedRevision),
200+
ObservedRequestID: strings.TrimSpace(requestBody.RequestID),
201+
}
202+
if binding.Annotations == nil {
203+
binding.Annotations = map[string]string{}
204+
}
205+
binding.Annotations[spritzv1.BindingReconcileRequestedAtAnnotationKey] = time.Now().UTC().Format(time.RFC3339Nano)
206+
207+
if createNew {
208+
if err := s.client.Create(c.Request().Context(), binding); err != nil {
209+
return writeError(c, http.StatusInternalServerError, err.Error())
210+
}
211+
} else {
212+
if err := s.client.Update(c.Request().Context(), binding); err != nil {
213+
return writeError(c, http.StatusInternalServerError, err.Error())
214+
}
215+
}
216+
217+
var stored spritzv1.SpritzBinding
218+
if err := s.client.Get(c.Request().Context(), client.ObjectKey{Namespace: namespace, Name: bindingName}, &stored); err != nil {
219+
return writeError(c, http.StatusInternalServerError, err.Error())
220+
}
221+
return writeJSON(c, http.StatusOK, summarizeInternalBinding(&stored))
222+
}
223+
224+
func (s *server) reconcileInternalBinding(c echo.Context) error {
225+
namespace, bindingName, err := s.resolveBindingPath(c)
226+
if err != nil {
227+
return writeError(c, http.StatusBadRequest, err.Error())
228+
}
229+
var binding spritzv1.SpritzBinding
230+
if err := s.client.Get(c.Request().Context(), client.ObjectKey{Namespace: namespace, Name: bindingName}, &binding); err != nil {
231+
if apierrors.IsNotFound(err) {
232+
return writeError(c, http.StatusNotFound, "not found")
233+
}
234+
return writeError(c, http.StatusInternalServerError, err.Error())
235+
}
236+
if binding.Annotations == nil {
237+
binding.Annotations = map[string]string{}
238+
}
239+
binding.Annotations[spritzv1.BindingReconcileRequestedAtAnnotationKey] = time.Now().UTC().Format(time.RFC3339Nano)
240+
if err := s.client.Update(c.Request().Context(), &binding); err != nil {
241+
return writeError(c, http.StatusInternalServerError, err.Error())
242+
}
243+
return writeJSON(c, http.StatusOK, summarizeInternalBinding(&binding))
244+
}
245+
246+
func (s *server) resolveBindingPath(c echo.Context) (string, string, error) {
247+
namespace, err := s.resolveSpritzNamespace(strings.TrimSpace(c.Param("namespace")))
248+
if err != nil {
249+
return "", "", err
250+
}
251+
bindingKey := strings.TrimSpace(c.Param("bindingKey"))
252+
if bindingKey == "" {
253+
return "", "", fmt.Errorf("bindingKey required")
254+
}
255+
return namespace, bindingResourceNameForKey(bindingKey), nil
256+
}
257+
258+
func convertInternalBindingRef(ref *internalBindingInstanceRef, defaultNamespace string) *spritzv1.SpritzBindingInstanceRef {
259+
if ref == nil {
260+
return nil
261+
}
262+
name := strings.TrimSpace(ref.InstanceID)
263+
if name == "" {
264+
return nil
265+
}
266+
namespace := strings.TrimSpace(ref.Namespace)
267+
if namespace == "" {
268+
namespace = defaultNamespace
269+
}
270+
return &spritzv1.SpritzBindingInstanceRef{
271+
Namespace: namespace,
272+
Name: name,
273+
Revision: strings.TrimSpace(ref.Revision),
274+
}
275+
}
276+
277+
func findBindingOwner(spritz *spritzv1.Spritz) string {
278+
if spritz == nil {
279+
return ""
280+
}
281+
for _, owner := range spritz.OwnerReferences {
282+
if strings.EqualFold(strings.TrimSpace(owner.Kind), "SpritzBinding") && owner.Name != "" {
283+
return owner.Name
284+
}
285+
}
286+
if spritz.Labels != nil {
287+
return strings.TrimSpace(spritz.Labels[spritzv1.BindingNameLabelKey])
288+
}
289+
return ""
290+
}
291+
292+
func (s *server) getBindingByRuntime(
293+
ctx context.Context,
294+
namespace string,
295+
source *spritzv1.Spritz,
296+
) (*spritzv1.SpritzBinding, error) {
297+
bindingName := findBindingOwner(source)
298+
if bindingName == "" {
299+
return nil, nil
300+
}
301+
var binding spritzv1.SpritzBinding
302+
if err := s.client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: bindingName}, &binding); err != nil {
303+
if apierrors.IsNotFound(err) {
304+
return nil, nil
305+
}
306+
return nil, err
307+
}
308+
return &binding, nil
309+
}
310+
311+
func (s *server) replaceInternalBinding(
312+
ctx context.Context,
313+
binding *spritzv1.SpritzBinding,
314+
targetRevision string,
315+
) (*spritzv1.SpritzBinding, bool, error) {
316+
if binding == nil {
317+
return nil, false, nil
318+
}
319+
replayed := strings.TrimSpace(binding.Spec.DesiredRevision) == strings.TrimSpace(targetRevision)
320+
if binding.Spec.DesiredRevision != strings.TrimSpace(targetRevision) {
321+
binding.Spec.DesiredRevision = strings.TrimSpace(targetRevision)
322+
if binding.Annotations == nil {
323+
binding.Annotations = map[string]string{}
324+
}
325+
binding.Annotations[spritzv1.BindingReconcileRequestedAtAnnotationKey] = time.Now().UTC().Format(time.RFC3339Nano)
326+
if err := s.client.Update(ctx, binding); err != nil {
327+
return nil, false, err
328+
}
329+
}
330+
var stored spritzv1.SpritzBinding
331+
if err := s.client.Get(ctx, client.ObjectKey{Namespace: binding.Namespace, Name: binding.Name}, &stored); err != nil {
332+
return nil, false, err
333+
}
334+
return &stored, replayed, nil
335+
}
336+
337+
func replacementRuntimeFromBinding(binding *spritzv1.SpritzBinding) *spritzv1.Spritz {
338+
if binding == nil {
339+
return nil
340+
}
341+
ref := binding.Status.CandidateInstanceRef
342+
if ref == nil && binding.Status.ActiveInstanceRef != nil && strings.TrimSpace(binding.Status.ObservedRevision) == strings.TrimSpace(binding.Spec.DesiredRevision) {
343+
ref = binding.Status.ActiveInstanceRef
344+
}
345+
if ref == nil {
346+
return nil
347+
}
348+
revision := strings.TrimSpace(ref.Revision)
349+
if revision == "" {
350+
revision = strings.TrimSpace(binding.Status.ObservedRevision)
351+
}
352+
return &spritzv1.Spritz{
353+
ObjectMeta: metav1.ObjectMeta{
354+
Namespace: ref.Namespace,
355+
Name: ref.Name,
356+
Annotations: map[string]string{
357+
targetRevisionAnnotationKey: revision,
358+
},
359+
},
360+
Status: spritzv1.SpritzStatus{
361+
Phase: strings.TrimSpace(ref.Phase),
362+
},
363+
}
364+
}

0 commit comments

Comments
 (0)