Skip to content

Commit

Permalink
Merge branch 'main' into support-fractional-cpu
Browse files Browse the repository at this point in the history
  • Loading branch information
k15r authored Feb 20, 2025
2 parents 4b43294 + a3e0864 commit 1bc9496
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 196 deletions.
33 changes: 33 additions & 0 deletions docs/contributor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,36 @@ See the following example payload:
}
}
```

## KEB Interface

KMC fetches the list of SKR clusters from KEB. KEB provides the list of SKR clusters regardless of their billable state.
KMC uses this list to populate an internal queue for processing.
Before attempting to add a cluster to the queue, KMC checks if the cluster is billable. If the cluster is not billable, KMC skips the cluster.

### Cluster Lifecycle
```mermaid
stateDiagram-v2
Running: Cluster is running
Suspended: Cluster is suspended
[*] --> Running: Provisioning
Running --> Running: Non-destructive operations
Running --> Suspended: Suspension
Suspended --> Running: Unsuspension
Running --> [*]: Deprovisioning
```

### Billing State
```mermaid
stateDiagram-v2
Billable: Cluster is billable
NonBillable: Cluster is not billable
[*] --> Billable: provisioned
Billable --> NonBillable: Cluster is suspended
NonBillable --> Billable: Cluster is unsuspended
Billable --> NonBillable: Cluster is deprovisioned
```
115 changes: 115 additions & 0 deletions pkg/process/cluster_tracking.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package process

import (
"sort"
"time"

kebruntime "github.com/kyma-project/kyma-environment-broker/common/runtime"
)

type runtimeState int

const (
provisioning runtimeState = iota
deprovisioning
upgradingkyma
upgradingcluster
update
suspension
unsuspension
)

type simpleOperation struct {
state runtimeState
time time.Time
succeeded bool
}

// isRuntimeTrackable determines if a runtime is trackable based on its operations.
// A runtime is considered trackable if:
// - It has a successful provisioning or unsuspension operation.
// - It does not have a suspension or deprovisioning operation as the last operation.
// - It has any other operation assuming the cluster was successfully provisioned and is billable.
func isRuntimeTrackable(runtime kebruntime.RuntimeDTO) bool {
// Sort operations by time
operations := sortOperations(runtime)

// If a cluster does not have any operations, it is not trackable
if len(operations) == 0 {
return false
}

// Get the last operation
lastOperation := operations[len(operations)-1]

// Determine trackability based on the last operation state
//nolint:exhaustive // we only care about the four states, not the others
switch lastOperation.state {
case provisioning, unsuspension:
return lastOperation.succeeded
case suspension, deprovisioning:
return false
default:
return true
}
}

func newSimpleOperation(state runtimeState, operation kebruntime.Operation) simpleOperation {
return simpleOperation{
state: state,
time: operation.CreatedAt,
succeeded: operationSucceeded(operation),
}
}

func operationSucceeded(operation kebruntime.Operation) bool {
return operation.State == string(kebruntime.StateSucceeded)
}

func sortOperations(runtime kebruntime.RuntimeDTO) []simpleOperation {
var operations []simpleOperation
if runtime.Status.Provisioning != nil {
operations = append(operations, newSimpleOperation(provisioning, *runtime.Status.Provisioning))
}

if runtime.Status.Deprovisioning != nil {
operations = append(operations, newSimpleOperation(deprovisioning, *runtime.Status.Deprovisioning))
}

if runtime.Status.UpgradingKyma != nil {
for _, op := range runtime.Status.UpgradingKyma.Data {
operations = append(operations, newSimpleOperation(upgradingkyma, op))
}
}

if runtime.Status.UpgradingCluster != nil {
for _, op := range runtime.Status.UpgradingCluster.Data {
operations = append(operations, newSimpleOperation(upgradingcluster, op))
}
}

if runtime.Status.Update != nil {
for _, op := range runtime.Status.Update.Data {
operations = append(operations, newSimpleOperation(update, op))
}
}

if runtime.Status.Suspension != nil {
for _, op := range runtime.Status.Suspension.Data {
operations = append(operations, newSimpleOperation(suspension, op))
}
}

if runtime.Status.Unsuspension != nil {
for _, op := range runtime.Status.Unsuspension.Data {
operations = append(operations, newSimpleOperation(unsuspension, op))
}
}

// sort operations by time
sort.Slice(operations, func(i, j int) bool {
return operations[i].time.Before(operations[j].time)
})

return operations
}
202 changes: 202 additions & 0 deletions pkg/process/cluster_tracking_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package process

import (
"testing"
"time"

kebruntime "github.com/kyma-project/kyma-environment-broker/common/runtime"
"github.com/onsi/gomega"
)

// TestSortOperations tests the sortOperations function.
func TestSortOperations(t *testing.T) {
g := gomega.NewGomegaWithT(t)

testCases := []struct {
name string
runtime kebruntime.RuntimeDTO
expected []runtimeState
expectedLen int
}{
{
name: "should sort operations by time",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Provisioning: &kebruntime.Operation{
CreatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
},
Deprovisioning: &kebruntime.Operation{
CreatedAt: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
},
UpgradingKyma: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC)},
}},
UpgradingCluster: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC)},
}},
Update: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Date(2023, 1, 5, 0, 0, 0, 0, time.UTC)},
}},
Suspension: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Date(2023, 1, 6, 0, 0, 0, 0, time.UTC)},
}},
Unsuspension: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Date(2023, 1, 7, 0, 0, 0, 0, time.UTC)},
}},
},
},
expected: []runtimeState{provisioning, deprovisioning, upgradingkyma, upgradingcluster, update, suspension, unsuspension},
expectedLen: 7,
},
{
name: "should handle multiple suspensions and following unsuspensions",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Suspension: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Date(2023, 1, 6, 0, 0, 0, 0, time.UTC)},
{CreatedAt: time.Date(2023, 1, 8, 0, 0, 0, 0, time.UTC)},
}},
Unsuspension: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Date(2023, 1, 7, 0, 0, 0, 0, time.UTC)},
{CreatedAt: time.Date(2023, 1, 9, 0, 0, 0, 0, time.UTC)},
}},
},
},
expected: []runtimeState{suspension, unsuspension, suspension, unsuspension},
expectedLen: 4,
},
{
name: "should handle empty operations",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{},
},
expected: []runtimeState{},
expectedLen: 0,
},
{
name: "should handle nil operation times",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Provisioning: &kebruntime.Operation{},
Deprovisioning: &kebruntime.Operation{},
},
},
expected: []runtimeState{provisioning, deprovisioning},
expectedLen: 2,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
operations := sortOperations(tc.runtime)

g.Expect(operations).To(gomega.HaveLen(tc.expectedLen))

for i, op := range operations {
g.Expect(op.state).To(gomega.Equal(tc.expected[i]))
}
})
}
}

func TestIsRuntimeTrackable(t *testing.T) {
g := gomega.NewGomegaWithT(t)

testCases := []struct {
name string
runtime kebruntime.RuntimeDTO
expected bool
}{
{
name: "should return true for successful provisioning",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Provisioning: &kebruntime.Operation{
CreatedAt: time.Now(),
State: string(kebruntime.StateSucceeded),
},
},
},
expected: true,
},
{
name: "should return false for suspension",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Suspension: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Now()},
}},
},
},
expected: false,
},
{
name: "should return false for failing provisioning",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Provisioning: &kebruntime.Operation{
CreatedAt: time.Now(),
State: string(kebruntime.StateFailed),
},
},
},
expected: false,
},
{
name: "should return false for failing unsuspension",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Unsuspension: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Now(), State: string(kebruntime.StateFailed)},
}},
},
},
expected: false,
},
{
name: "should return true for successful unsuspension",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Unsuspension: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Now(), State: string(kebruntime.StateSucceeded)},
}},
},
},
expected: true,
},
{
name: "should return false for deprovisioning",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Deprovisioning: &kebruntime.Operation{CreatedAt: time.Now()},
},
},
expected: false,
},
{
name: "should return false for empty operations",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{},
},
expected: false,
},
{
name: "should return true for other operations",
runtime: kebruntime.RuntimeDTO{
Status: kebruntime.RuntimeStatus{
Update: &kebruntime.OperationsData{Data: []kebruntime.Operation{
{CreatedAt: time.Now()},
}},
},
},
expected: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := isRuntimeTrackable(tc.runtime)
g.Expect(result).To(gomega.Equal(tc.expected))
})
}
}
30 changes: 0 additions & 30 deletions pkg/process/keb_processing.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,36 +177,6 @@ func getOrDefault(runtimeStatus *kebruntime.Operation, defaultValue string) stri
return defaultValue
}

func isRuntimeTrackable(runtime kebruntime.RuntimeDTO) bool {
if runtime.Status.State == kebruntime.StateDeprovisioning {
return false
}

return isTrackableState(runtime.Status.State) || isProvisionedStatus(runtime)
}

// isProvisionedStatus returns true if the runtime is successfully provisioned, otherwise returns false.
func isProvisionedStatus(runtime kebruntime.RuntimeDTO) bool {
if runtime.Status.Provisioning != nil &&
runtime.Status.Provisioning.State == string(kebruntime.StateSucceeded) &&
runtime.Status.Deprovisioning == nil {
return true
}

return false
}

// isTrackableState returns true if the runtime state is trackable, otherwise returns false.
func isTrackableState(state kebruntime.State) bool {
//nolint:exhaustive // we only care about these states
switch state {
case kebruntime.StateSucceeded, kebruntime.StateError, kebruntime.StateUpgrading, kebruntime.StateUpdating:
return true
}

return false
}

func (p *Process) namedLogger() *zap.SugaredLogger {
return p.Logger.With("component", "kmc")
}
Expand Down
Loading

0 comments on commit 1bc9496

Please sign in to comment.