Skip to content

Commit d8176a7

Browse files
eshulman2claude
andcommitted
Add AI agent instructions and skills using open standards
Add AGENTS.md with project-specific instructions for AI-assisted development of ORC controllers, including project structure, key patterns, and references to detailed documentation. CLAUDE.md is a symlink for Claude Code compatibility. Add skills for common development workflows in .agents/skills/: - /new-controller: Scaffold and implement new ORC controllers - /update-controller: Modify existing controllers (add fields, tags, etc.) - /add-dependency: Add resource dependencies to controllers - /proposal: Write enhancement proposals following the template - /testing: Run unit tests, linting, and E2E tests Skills follow the Agent Skills open standard (SKILL.md format). .claude/skills/ symlinks to .agents/skills/ for Claude Code support. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 179afeb commit d8176a7

File tree

10 files changed

+1553
-0
lines changed

10 files changed

+1553
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
---
2+
name: add-dependency
3+
description: Add a dependency on another ORC resource to a controller. Use when a resource needs to reference or wait for another resource (e.g., Subnet depends on Network).
4+
disable-model-invocation: true
5+
---
6+
7+
# Add Dependency to Controller
8+
9+
Guide for adding a dependency on another ORC resource.
10+
11+
**Reference**: See `website/docs/development/controller-implementation.md` for detailed rationale on dependency patterns.
12+
13+
## When to Use Dependencies
14+
15+
Use a dependency when your controller needs to:
16+
- Wait for another resource to be available before creating
17+
- Reference another resource's OpenStack ID
18+
- Optionally prevent deletion of a resource that's still in use (deletion guard)
19+
20+
## Key Principles
21+
22+
See also "Dependency Timing" in @.agents/skills/new-controller/patterns.md
23+
24+
### 1. Resolve Dependencies Late
25+
26+
Resolve dependencies as late as possible, as close to the point of use as possible. This reduces coupling and gives users flexibility when fixing failed deployments.
27+
28+
**Examples:**
29+
- Subnet depends on Network for creation, but NOT for import by ID or after `status.ID` is set
30+
- Don't require recreating a deleted Network just to delete a Subnet
31+
- Add finalizers only immediately before the OpenStack create/update call
32+
33+
### 2. Choose the Right Dependency Type
34+
35+
| Type | Use When | Example |
36+
|------|----------|---------|
37+
| **Normal** (`NewDependency`) | Dependency is optional OR deletion is allowed by OpenStack | Import filter refs, Flavor ref |
38+
| **Deletion Guard** (`NewDeletionGuardDependency`) | Deletion would fail or corrupt your resource | Subnet→Network, Port→Subnet |
39+
40+
### 3. Use Descriptive Names
41+
42+
When multiple dependencies of the same type exist, use descriptive prefixes:
43+
- `vipSubnetDependency` not `subnetDependency` (when there could be other subnet refs)
44+
- `sourcePortDependency` vs `destinationPortDependency`
45+
- `memberNetworkDependency` vs `externalNetworkDependency`
46+
47+
## Dependency Types
48+
49+
### Normal Dependency
50+
Wait for resource but don't prevent deletion:
51+
```go
52+
dependency.NewDependency[*orcv1alpha1.MyResourceList, *orcv1alpha1.DepResource](...)
53+
```
54+
55+
### Deletion Guard Dependency
56+
Wait for resource AND prevent its deletion:
57+
```go
58+
dependency.NewDeletionGuardDependency[*orcv1alpha1.MyResourceList, *orcv1alpha1.DepResource](...)
59+
```
60+
61+
**Use Deletion Guard when**: Deleting the dependency would cause your resource to fail or become invalid (e.g., Subnet depends on Network, Port depends on SecurityGroup).
62+
63+
## Step 1: Add Reference Field to API
64+
65+
In `api/v1alpha1/<kind>_types.go`, add the reference field:
66+
67+
```go
68+
type MyResourceSpec struct {
69+
// ...
70+
71+
// projectRef is a reference to a Project.
72+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable"
73+
// +optional
74+
ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"`
75+
}
76+
```
77+
78+
For import filters, add to the Filter struct as well:
79+
```go
80+
type MyResourceFilter struct {
81+
// +optional
82+
ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"`
83+
}
84+
```
85+
86+
## Step 2: Declare Dependency
87+
88+
In `internal/controllers/<kind>/controller.go`, add package-scoped variable:
89+
90+
```go
91+
var (
92+
projectDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.MyResourceList, *orcv1alpha1.Project](
93+
"spec.resource.projectRef", // Field path for indexing
94+
func(obj *orcv1alpha1.MyResource) []string {
95+
resource := obj.Spec.Resource
96+
if resource == nil || resource.ProjectRef == nil {
97+
return nil
98+
}
99+
return []string{string(*resource.ProjectRef)}
100+
},
101+
finalizer, externalObjectFieldOwner,
102+
)
103+
104+
// For import filter dependencies (no deletion guard needed)
105+
projectImportDependency = dependency.NewDependency[*orcv1alpha1.MyResourceList, *orcv1alpha1.Project](
106+
"spec.import.filter.projectRef",
107+
func(obj *orcv1alpha1.MyResource) []string {
108+
imp := obj.Spec.Import
109+
if imp == nil || imp.Filter == nil || imp.Filter.ProjectRef == nil {
110+
return nil
111+
}
112+
return []string{string(*imp.Filter.ProjectRef)}
113+
},
114+
)
115+
)
116+
```
117+
118+
## Step 3: Setup Watches
119+
120+
In `SetupWithManager()` in `controller.go`:
121+
122+
```go
123+
func (c myReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
124+
log := ctrl.LoggerFrom(ctx)
125+
k8sClient := mgr.GetClient()
126+
127+
// Create watch handlers
128+
projectWatchHandler, err := projectDependency.WatchEventHandler(log, k8sClient)
129+
if err != nil {
130+
return err
131+
}
132+
133+
builder := ctrl.NewControllerManagedBy(mgr).
134+
WithOptions(options).
135+
For(&orcv1alpha1.MyResource{}).
136+
// Watch the dependency
137+
Watches(&orcv1alpha1.Project{}, projectWatchHandler,
138+
builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Project{})),
139+
)
140+
141+
// Register dependencies with manager
142+
if err := errors.Join(
143+
projectDependency.AddToManager(ctx, mgr),
144+
credentialsDependency.AddToManager(ctx, mgr),
145+
credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency),
146+
); err != nil {
147+
return err
148+
}
149+
150+
r := reconciler.NewController(controllerName, k8sClient, c.scopeFactory, helperFactory{}, statusWriter{})
151+
return builder.Complete(&r)
152+
}
153+
```
154+
155+
## Step 4: Use Dependency in Actuator
156+
157+
In `actuator.go`, resolve the dependency before using it:
158+
159+
```go
160+
func (actuator myActuator) CreateResource(ctx context.Context, obj *orcv1alpha1.MyResource) (*osResourceT, progress.ReconcileStatus) {
161+
resource := obj.Spec.Resource
162+
163+
var projectID string
164+
if resource.ProjectRef != nil {
165+
project, reconcileStatus := projectDependency.GetDependency(
166+
ctx, actuator.k8sClient, obj,
167+
func(dep *orcv1alpha1.Project) bool {
168+
return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
169+
},
170+
)
171+
if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
172+
return nil, reconcileStatus
173+
}
174+
projectID = ptr.Deref(project.Status.ID, "")
175+
}
176+
177+
createOpts := myresource.CreateOpts{
178+
ProjectID: projectID,
179+
// ...
180+
}
181+
// ...
182+
}
183+
```
184+
185+
For import filter dependencies:
186+
```go
187+
func (actuator myActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) {
188+
var reconcileStatus progress.ReconcileStatus
189+
190+
project, rs := dependency.FetchDependency(
191+
ctx, actuator.k8sClient, obj.Namespace, filter.ProjectRef, "Project",
192+
func(dep *orcv1alpha1.Project) bool {
193+
return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
194+
},
195+
)
196+
reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
197+
198+
if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
199+
return nil, reconcileStatus
200+
}
201+
202+
listOpts := myresource.ListOpts{
203+
ProjectID: ptr.Deref(project.Status.ID, ""),
204+
}
205+
return actuator.osClient.ListMyResources(ctx, listOpts), nil
206+
}
207+
```
208+
209+
## Step 5: Add k8sClient to Actuator
210+
211+
If not already present, add `k8sClient` to the actuator struct:
212+
213+
```go
214+
type myActuator struct {
215+
osClient osclients.MyResourceClient
216+
k8sClient client.Client // Add this
217+
}
218+
```
219+
220+
Update `newActuator()`:
221+
```go
222+
func newActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (myActuator, progress.ReconcileStatus) {
223+
k8sClient := controller.GetK8sClient() // Add this
224+
// ...
225+
return myActuator{
226+
osClient: osClient,
227+
k8sClient: k8sClient, // Add this
228+
}, nil
229+
}
230+
```
231+
232+
## Step 6: Add Tests
233+
234+
Create dependency tests in `internal/controllers/<kind>/tests/<kind>-dependency/`:
235+
- Test that resource waits for dependency
236+
- Test that dependency deletion is blocked (if using DeletionGuard)
237+
238+
Follow @.agents/skills/testing/SKILL.md for running unit tests, linting, and E2E tests.
239+
240+
## Checklist
241+
242+
- [ ] Reference field added to API types (with immutability validation)
243+
- [ ] Dependency declared in controller.go
244+
- [ ] Watch configured in SetupWithManager
245+
- [ ] Dependency registered with manager (AddToManager)
246+
- [ ] Dependency resolved in actuator before use
247+
- [ ] k8sClient added to actuator struct
248+
- [ ] `make generate` runs cleanly
249+
- [ ] `make lint` passes
250+
- [ ] Dependency tests added

0 commit comments

Comments
 (0)