feat: implement reconciliation loop (#17261)

Closes https://github.com/coder/internal/issues/510

<details>
<summary> Refactoring Summary </summary>

### 1) `CalculateActions` Function

#### Issues Before Refactoring:

- Large function (~150 lines), making it difficult to read and maintain.
- The control flow is hard to follow due to complex conditional logic.
- The `ReconciliationActions` struct was partially initialized early,
then mutated in multiple places, making the flow error-prone.

Original source:  

fe60b569ad/coderd/prebuilds/state.go (L13-L167)

#### Improvements After Refactoring:

- Simplified and broken down into smaller, focused helper methods.
- The flow of the function is now more linear and easier to understand.
- Struct initialization is cleaner, avoiding partial and incremental
mutations.

Refactored function:  

eeb0407d78/coderd/prebuilds/state.go (L67-L84)

---

### 2) `ReconciliationActions` Struct

#### Issues Before Refactoring:

- The struct mixed both actionable decisions and diagnostic state, which
blurred its purpose.
- It was unclear which fields were necessary for reconciliation logic,
and which were purely for logging/observability.

#### Improvements After Refactoring:

- Split into two clear, purpose-specific structs:
- **`ReconciliationActions`** — defines the intended reconciliation
action.
- **`ReconciliationState`** — captures runtime state and metadata,
primarily for logging and diagnostics.

Original struct:  

fe60b569ad/coderd/prebuilds/reconcile.go (L29-L41)

</details>

---------

Signed-off-by: Danny Kopping <dannykopping@gmail.com>
Co-authored-by: Sas Swart <sas.swart.cdk@gmail.com>
Co-authored-by: Danny Kopping <dannykopping@gmail.com>
Co-authored-by: Dean Sheather <dean@deansheather.com>
Co-authored-by: Spike Curtis <spike@coder.com>
Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
Yevhenii Shcherbina
2025-04-17 09:29:29 -04:00
committed by GitHub
parent 6a79965948
commit 27bc60d1b9
16 changed files with 2834 additions and 9 deletions

View File

@ -2,6 +2,7 @@ package slice_test
import (
"math/rand"
"strings"
"testing"
"github.com/google/uuid"
@ -82,6 +83,64 @@ func TestContains(t *testing.T) {
)
}
func TestFilter(t *testing.T) {
t.Parallel()
type testCase[T any] struct {
haystack []T
cond func(T) bool
expected []T
}
{
testCases := []*testCase[int]{
{
haystack: []int{1, 2, 3, 4, 5},
cond: func(num int) bool {
return num%2 == 1
},
expected: []int{1, 3, 5},
},
{
haystack: []int{1, 2, 3, 4, 5},
cond: func(num int) bool {
return num%2 == 0
},
expected: []int{2, 4},
},
}
for _, tc := range testCases {
actual := slice.Filter(tc.haystack, tc.cond)
require.Equal(t, tc.expected, actual)
}
}
{
testCases := []*testCase[string]{
{
haystack: []string{"hello", "hi", "bye"},
cond: func(str string) bool {
return strings.HasPrefix(str, "h")
},
expected: []string{"hello", "hi"},
},
{
haystack: []string{"hello", "hi", "bye"},
cond: func(str string) bool {
return strings.HasPrefix(str, "b")
},
expected: []string{"bye"},
},
}
for _, tc := range testCases {
actual := slice.Filter(tc.haystack, tc.cond)
require.Equal(t, tc.expected, actual)
}
}
}
func TestOverlap(t *testing.T) {
t.Parallel()