okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(cli): share cursor traversal across paged resources155c8e8

main
8.3 KiB213 linesraw
1package daemon
2
3import (
4	"context"
5	"errors"
6	"io"
7	"net/http"
8	"regexp"
9
10	"albedo/cli/internal/daemon/protocol"
11)
12
13type Webhook struct {
14	ID, Session, Name, URL, Address, Header, Prefix, Revision, ETag string
15	Enabled                                                         bool
16}
17type WebhookEntry struct {
18	Deferred *string
19	Hook     Webhook
20	Queued   int
21}
22type WebhookResult struct {
23	Hook                                                *Webhook
24	Session, Secret, Message, PermissionETag, DeletedID string
25	Hooks                                               []WebhookEntry
26	AgentManagement                                     bool
27}
28type WebhookPermission struct {
29	SessionID, ETag string
30	AgentManagement bool
31}
32type WebhookCreateRequest = protocol.HookCreate
33type WebhookPatch = protocol.HookPatch
34type WebhookAction string
35
36const (
37	WebhookCreate       WebhookAction = "create"
38	WebhookSignature    WebhookAction = "signature"
39	WebhookRotate       WebhookAction = "rotate"
40	WebhookEnable       WebhookAction = "enable"
41	WebhookDisable      WebhookAction = "disable"
42	WebhookDelete       WebhookAction = "delete"
43	WebhookAgentEnable  WebhookAction = "agent_enable"
44	WebhookAgentDisable WebhookAction = "agent_disable"
45)
46
47// WebhookRequest is a captured terminal form operation, not a public dispatcher.
48type WebhookRequest struct {
49	Action                                                WebhookAction
50	HookID, SessionID, Name, Secret, Header, Prefix, ETag string
51}
52
53var webhookNamePattern = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
54
55func hookValue(w protocol.HookConfigurationResource, deliveryURL string) Webhook {
56	c := w.Value
57	return Webhook{ID: c.ID, Session: c.SessionID, Name: c.Name, URL: deliveryURL, Address: deliveryURL, Header: c.SignatureHeader, Prefix: c.SignaturePrefix, Revision: c.Revision, ETag: w.ETag, Enabled: c.Enabled}
58}
59func ListWebhooks(ctx context.Context, conn *Connection, sessionID string) ([]WebhookEntry, error) {
60	r := []WebhookEntry{}
61	params := protocol.ListHooksParams{Limit: new(int64(200))}
62	if sessionID != "" {
63		params.SessionID = &sessionID
64	}
65	err := walkPages(func(next *string) (protocol.HookPage, *string, error) {
66		params.Next = next
67		var page protocol.HookPage
68		pageErr := executeRead(ctx, conn, operation{Name: "list webhooks", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
69			return protocol.NewListHooksRequest(base, &params)
70		}, Policy: readRecovery}, func(data []byte) error { return decodeRequired(data, &page) })
71		return page, page.Next, pageErr
72	}, func(page protocol.HookPage) error {
73		if page.Items == nil {
74			return fieldError("hooks")
75		}
76		for _, row := range page.Items {
77			if row.ConfigurationResource.ETag == "" {
78				return fieldError("hook validator")
79			}
80			var deferred *string
81			if row.DeferralReason != nil {
82				deferred = &row.DeferralReason.Detail
83			}
84			r = append(r, WebhookEntry{Hook: hookValue(row.ConfigurationResource, row.DeliveryURL), Queued: int(row.PendingCount), Deferred: deferred})
85		}
86		return nil
87	}, "hook page cursor")
88	if err != nil {
89		return nil, err
90	}
91	return r, nil
92}
93
94func hookChange(ctx context.Context, conn *Connection, op operation, status int, secret bool) (*WebhookResult, error) {
95	var w protocol.HookChange
96	err := executeMutation(ctx, conn, op, []int{status}, func(data []byte, _ int) error {
97		if err := decodeRequired(data, &w); err != nil {
98			return err
99		}
100		if w.Resource.ETag == "" || w.Resource.Value.ID == "" {
101			return fieldError("hook resource")
102		}
103		if secret && value(w.Secret) == "" {
104			return fieldError("hook secret")
105		}
106		if !secret && w.Secret != nil {
107			return fieldError("unexpected hook secret")
108		}
109		return nil
110	})
111	if err != nil {
112		return nil, err
113	}
114	h := hookValue(w.Resource, "")
115	r := &WebhookResult{Hook: &h, Secret: value(w.Secret), Message: "Saved."}
116	if w.Notification.State == "failed" {
117		r.Message = "Saved; notification failed: " + value(w.Notification.Detail)
118	}
119	return r, nil
120}
121func CreateWebhook(ctx context.Context, conn *Connection, request WebhookCreateRequest) (*WebhookResult, error) {
122	if !webhookNamePattern.MatchString(request.Name) || request.SessionID == "" {
123		return nil, errors.New("webhook creation requires a session and a valid name")
124	}
125	return hookChange(ctx, conn, operation{Name: "create webhook", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
126		return protocol.NewCreateHookRequest(base, request)
127	}, Body: request, Policy: noRecovery}, 201, true)
128}
129func EditWebhook(ctx context.Context, conn *Connection, id, etag string, patch WebhookPatch) (*WebhookResult, error) {
130	headers, err := observedHeaders(etag)
131	if err != nil {
132		return nil, err
133	}
134	return hookChange(ctx, conn, operation{Name: "edit webhook", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
135		return protocol.NewPatchHookRequestWithApplicationMergePatchPlusJSONBody(base, id, &protocol.PatchHookParams{View: "configuration", IfMatch: etag}, patch)
136	}, Headers: headers, Body: patch, Policy: noRecovery}, 200, false)
137}
138func RotateWebhookSecret(ctx context.Context, conn *Connection, id, etag, secret string) (*WebhookResult, error) {
139	headers, err := observedHeaders(etag)
140	if err != nil {
141		return nil, err
142	}
143	headers.Set("Content-Type", "application/json")
144	return hookChange(ctx, conn, operation{Name: "rotate webhook secret", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
145		return protocol.NewRotateHookSecretRequestWithBody(base, id, &protocol.RotateHookSecretParams{IfMatch: etag}, "application/json", body)
146	}, Headers: headers, Body: protocol.HookSecretRequest{Secret: optionalText(secret)}, Policy: noRecovery}, 200, true)
147}
148func DeleteWebhook(ctx context.Context, conn *Connection, id, etag string) (*WebhookResult, error) {
149	headers, err := observedHeaders(etag)
150	if err != nil {
151		return nil, err
152	}
153	var w protocol.ExtensionDeletion
154	err = executeMutation(ctx, conn, operation{Name: "delete webhook", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
155		return protocol.NewDeleteHookRequest(base, id, &protocol.DeleteHookParams{View: "configuration", IfMatch: etag})
156	}, Headers: headers, Policy: noRecovery}, []int{200}, func(data []byte, _ int) error {
157		if err := decodeRequired(data, &w); err != nil {
158			return err
159		}
160		if w.ID != id {
161			return fieldError("deleted hook")
162		}
163		return nil
164	})
165	r := &WebhookResult{Message: "Deleted.", DeletedID: w.ID}
166	if w.Notification.State == "failed" {
167		r.Message += " Notification failed: " + value(w.Notification.Detail)
168	}
169	return r, err
170}
171func GetWebhookPermission(ctx context.Context, conn *Connection, id string) (WebhookPermission, error) {
172	r := WebhookPermission{SessionID: id}
173	var w protocol.HookPermission
174	err := executeRead(ctx, conn, operation{Name: "read webhook agent permission", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
175		return protocol.NewGetHookPermissionRequest(base, id)
176	}, Validator: &r.ETag, Policy: readRecovery}, func(data []byte) error {
177		if err := decodeRequired(data, &w); err != nil {
178			return err
179		}
180		if w.SessionID != id || r.ETag == "" {
181			return fieldError("webhook permission")
182		}
183		r.AgentManagement = w.AgentManage
184		return nil
185	})
186	return r, err
187}
188func SetWebhookPermission(ctx context.Context, conn *Connection, id, etag string, enabled bool) (*WebhookResult, error) {
189	headers, err := observedHeaders(etag)
190	if err != nil {
191		return nil, err
192	}
193	var w protocol.HookPermissionChange
194	err = executeMutation(ctx, conn, operation{Name: "edit webhook agent permission", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
195		return protocol.NewPatchHookPermissionRequestWithBody(base, id, &protocol.PatchHookPermissionParams{IfMatch: etag}, "application/merge-patch+json", body)
196	}, Headers: headers, Body: protocol.HookPermissionPatch{AgentManage: &enabled}, Policy: noRecovery}, []int{200}, func(data []byte, _ int) error {
197		if err := decodeRequired(data, &w); err != nil {
198			return err
199		}
200		if w.Resource.Value.SessionID != id || w.Resource.ETag == "" || w.Resource.Value.AgentManage != enabled {
201			return fieldError("webhook permission acknowledgment")
202		}
203		return nil
204	})
205	if err != nil {
206		return nil, err
207	}
208	result := &WebhookResult{AgentManagement: w.Resource.Value.AgentManage, PermissionETag: w.Resource.ETag, Message: "Saved agent permission."}
209	if w.Notification.State == "failed" {
210		result.Message += " Notification failed: " + value(w.Notification.Detail)
211	}
212	return result, nil
213}