-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathservice_projects.go
More file actions
252 lines (227 loc) · 7.6 KB
/
Copy pathservice_projects.go
File metadata and controls
252 lines (227 loc) · 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
package kata
import (
"context"
"errors"
"fmt"
"strings"
"time"
"go.kenn.io/kata/internal/config"
"go.kenn.io/kata/internal/daemon"
"go.kenn.io/kata/internal/db"
katauid "go.kenn.io/kata/internal/uid"
)
// ErrProjectConflict reports that a requested stable project identity or name
// is already bound to a different project.
var ErrProjectConflict = errors.New("kata: project identity conflict")
// ErrProjectNotFound reports that no host-visible project has the requested
// stable identity.
var ErrProjectNotFound = errors.New("kata: project not found")
// ProjectState describes whether a host-managed project is available for
// ordinary operations.
type ProjectState string
const (
// ProjectActive is visible to ordinary task operations.
ProjectActive ProjectState = "active"
// ProjectArchived retains history but is hidden from ordinary operations.
ProjectArchived ProjectState = "archived"
)
// Project is the stable project identity returned to an embedding host.
type Project struct {
ID int64
UID string
Name string
State ProjectState
CreatedAt time.Time
}
// ProjectSpec identifies the exact project a host wants to exist.
type ProjectSpec struct {
UID string
Name string
}
// EnsureProjectResult reports the stable project and whether this call
// created it.
type EnsureProjectResult struct {
Project Project
Created bool
}
// ProjectMutationResult reports the stable project and whether the call
// changed its lifecycle.
type ProjectMutationResult struct {
Project Project
Changed bool
}
// EnsureProject creates the exact requested project or returns the existing
// exact binding. Reusing either the UID or name for a different project fails
// with ErrProjectConflict. Archived projects are returned without reactivation.
func (s *Service) EnsureProject(ctx context.Context, spec ProjectSpec) (EnsureProjectResult, error) {
if err := validateProjectSpec(spec); err != nil {
return EnsureProjectResult{}, err
}
callCtx, done, err := s.beginHostCall(ctx)
if err != nil {
return EnsureProjectResult{}, err
}
defer done()
if existing, found, err := s.projectByUID(callCtx, spec.UID); err != nil {
return EnsureProjectResult{}, err
} else if found {
return exactProjectResult(existing, spec)
}
if existing, found, err := s.projectByName(callCtx, spec.Name); err != nil {
return EnsureProjectResult{}, err
} else if found {
return exactProjectResult(existing, spec)
}
created, event, createErr := s.store.CreateProjectWithUIDAndEvent(
callCtx, spec.Name, spec.UID, db.SystemActor,
)
if createErr == nil {
s.broadcaster.Broadcast(daemon.StreamMsg{
Kind: "event", Event: &event, ProjectID: created.ID,
})
s.hookSink.Enqueue(event)
return EnsureProjectResult{Project: publicProject(created), Created: true}, nil
}
// Another service instance may have created the same identity after both
// lookups. Collapse that race into the same idempotent result.
if existing, found, lookupErr := s.projectByUID(callCtx, spec.UID); lookupErr != nil {
return EnsureProjectResult{}, errors.Join(createErr, lookupErr)
} else if found {
return exactProjectResult(existing, spec)
}
if existing, found, lookupErr := s.projectByName(callCtx, spec.Name); lookupErr != nil {
return EnsureProjectResult{}, errors.Join(createErr, lookupErr)
} else if found {
return exactProjectResult(existing, spec)
}
return EnsureProjectResult{}, fmt.Errorf("kata: ensure project: %w", createErr)
}
// ArchiveProject hides a project from ordinary operations while retaining its
// stable identity, task history, and events. Repeating the exact archive is an
// idempotent no-op.
func (s *Service) ArchiveProject(
ctx context.Context,
projectUID string,
actor string,
) (ProjectMutationResult, error) {
if !katauid.Valid(projectUID) || projectUID == db.SystemProjectUID {
return ProjectMutationResult{}, ErrProjectNotFound
}
if strings.TrimSpace(actor) == "" {
return ProjectMutationResult{}, errors.New("kata: archive actor is required")
}
callCtx, done, err := s.beginHostCall(ctx)
if err != nil {
return ProjectMutationResult{}, err
}
defer done()
existing, found, err := s.projectByUID(callCtx, projectUID)
if err != nil {
return ProjectMutationResult{}, err
}
if !found {
return ProjectMutationResult{}, ErrProjectNotFound
}
if existing.DeletedAt != nil {
return ProjectMutationResult{Project: publicProject(existing)}, nil
}
archived, event, err := s.store.RemoveProject(callCtx, db.RemoveProjectParams{
ProjectID: existing.ID, Actor: actor, Force: true,
})
if errors.Is(err, db.ErrProjectAlreadyArchived) {
converged, found, lookupErr := s.projectByUID(callCtx, projectUID)
if lookupErr != nil {
return ProjectMutationResult{}, lookupErr
}
if !found {
return ProjectMutationResult{}, ErrProjectNotFound
}
return ProjectMutationResult{Project: publicProject(converged)}, nil
}
if errors.Is(err, db.ErrNotFound) {
return ProjectMutationResult{}, ErrProjectNotFound
}
if err != nil {
return ProjectMutationResult{}, fmt.Errorf("kata: archive project: %w", err)
}
if event != nil {
s.broadcaster.Broadcast(daemon.StreamMsg{
Kind: "event", Event: event, ProjectID: archived.ID,
})
s.hookSink.Enqueue(*event)
}
return ProjectMutationResult{Project: publicProject(archived), Changed: true}, nil
}
func validateProjectSpec(spec ProjectSpec) error {
if !katauid.Valid(spec.UID) || spec.UID == db.SystemProjectUID {
return fmt.Errorf("kata: invalid project UID")
}
if spec.Name == db.SystemProjectName || strings.Contains(spec.Name, "#") {
return fmt.Errorf("kata: invalid project name")
}
if err := config.ValidateProjectName(spec.Name); err != nil {
return fmt.Errorf("kata: invalid project name: %w", err)
}
return nil
}
func (s *Service) projectByUID(ctx context.Context, uid string) (db.Project, bool, error) {
project, err := s.store.ProjectByUID(ctx, uid)
if errors.Is(err, db.ErrNotFound) {
return db.Project{}, false, nil
}
if err != nil {
return db.Project{}, false, fmt.Errorf("kata: find project by UID: %w", err)
}
return project, true, nil
}
func (s *Service) projectByName(ctx context.Context, name string) (db.Project, bool, error) {
project, err := s.store.ProjectByNameIncludingArchived(ctx, name)
if errors.Is(err, db.ErrNotFound) {
return db.Project{}, false, nil
}
if err != nil {
return db.Project{}, false, fmt.Errorf("kata: find project by name: %w", err)
}
return project, true, nil
}
func exactProjectResult(existing db.Project, spec ProjectSpec) (EnsureProjectResult, error) {
if existing.UID != spec.UID || existing.Name != spec.Name {
return EnsureProjectResult{}, projectConflict(spec)
}
return EnsureProjectResult{Project: publicProject(existing)}, nil
}
func projectConflict(spec ProjectSpec) error {
return fmt.Errorf("%w: requested UID %q and name %q do not identify the same project",
ErrProjectConflict, spec.UID, spec.Name)
}
func publicProject(project db.Project) Project {
state := ProjectActive
if project.DeletedAt != nil {
state = ProjectArchived
}
return Project{
ID: project.ID, UID: project.UID, Name: project.Name,
State: state, CreatedAt: project.CreatedAt,
}
}
func (s *Service) beginHostCall(ctx context.Context) (context.Context, func(), error) {
if ctx == nil {
return nil, nil, errors.New("kata: context is required")
}
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil, nil, errors.New("kata: service is closed")
}
s.handlerWG.Add(1)
lifetimeCtx := s.lifetimeCtx
s.mu.Unlock()
callCtx, cancel := context.WithCancel(ctx)
stopLifetimeCancel := context.AfterFunc(lifetimeCtx, cancel)
done := func() {
stopLifetimeCancel()
cancel()
s.handlerWG.Done()
}
return callCtx, done, nil
}