Skip to content

Commit

Permalink
feat(build): Add Build waiting condition
Browse files Browse the repository at this point in the history
* Add on Build.Status a "Scheduled" condition containing the reason for it's scheduling status

Ref #4542
  • Loading branch information
gansheer authored and christophd committed Feb 28, 2024
1 parent 6f1dadc commit 4b38fc1
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 96 deletions.
73 changes: 73 additions & 0 deletions e2e/builder/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

. "github.com/apache/camel-k/v2/e2e/support"
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
corev1 "k8s.io/api/core/v1"
)

type kitOptions struct {
Expand Down Expand Up @@ -131,8 +132,10 @@ func TestKitMaxBuildLimit(t *testing.T) {
// verify that all builds are successful
Eventually(BuildPhase(ns, buildA), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns, buildA), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

Eventually(BuildPhase(ns1, buildB), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns1, buildB), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

Eventually(BuildPhase(ns2, buildC), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns2, buildC), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))
})
Expand Down Expand Up @@ -288,6 +291,76 @@ func TestKitMaxBuildLimitDependencyMatchingStrategy(t *testing.T) {
})
}

func TestMaxBuildLimitWaitingBuilds(t *testing.T) {
WithNewTestNamespace(t, func(ns string) {
createOperator(ns, "8m0s", "--global", "--force")

pl := Platform(ns)()
// set maximum number of running builds and order strategy
pl.Spec.Build.MaxRunningBuilds = 1
pl.Spec.Build.BuildConfiguration.OrderStrategy = v1.BuildOrderStrategyFIFO
if err := TestClient().Update(TestContext, pl); err != nil {
t.Error(err)
t.FailNow()
}

buildA := "integration-a"
buildB := "integration-b"
buildC := "integration-c"

doKitBuildInNamespace(buildA, ns, TestTimeoutShort, kitOptions{
operatorID: fmt.Sprintf("camel-k-%s", ns),
dependencies: []string{
"camel:timer", "camel:log",
},
traits: []string{
"builder.properties=build-property=A",
},
}, v1.BuildPhaseRunning, v1.IntegrationKitPhaseBuildRunning)

doKitBuildInNamespace(buildB, ns, TestTimeoutShort, kitOptions{
operatorID: fmt.Sprintf("camel-k-%s", ns),
dependencies: []string{
"camel:cron", "camel:log", "camel:joor",
},
traits: []string{
"builder.properties=build-property=B",
},
}, v1.BuildPhaseScheduling, v1.IntegrationKitPhaseNone)

doKitBuildInNamespace(buildC, ns, TestTimeoutShort, kitOptions{
operatorID: fmt.Sprintf("camel-k-%s", ns),
dependencies: []string{
"camel:timer", "camel:log", "camel:joor", "camel:http",
},
traits: []string{
"builder.properties=build-property=C",
},
}, v1.BuildPhaseScheduling, v1.IntegrationKitPhaseNone)

// verify that last build is waiting
Eventually(BuildConditions(ns, buildC), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(
BuildCondition(ns, buildC, v1.BuildConditionType(v1.BuildConditionScheduled))().Status,
TestTimeoutShort).Should(Equal(corev1.ConditionFalse))
Eventually(
BuildCondition(ns, buildC, v1.BuildConditionType(v1.BuildConditionScheduled))().Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionWaitingReason))

// verify that last build is scheduled
Eventually(BuildPhase(ns, buildC), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns, buildC), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

Eventually(BuildConditions(ns, buildC), TestTimeoutLong).ShouldNot(BeNil())
Eventually(
BuildCondition(ns, buildC, v1.BuildConditionType(v1.BuildConditionScheduled))().Status,
TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
Eventually(
BuildCondition(ns, buildC, v1.BuildConditionType(v1.BuildConditionScheduled))().Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionReadyReason))
})
}

func TestKitTimerToLogFullBuild(t *testing.T) {
doKitFullBuild(t, "timer-to-log", "8m0s", TestTimeoutLong, kitOptions{
dependencies: []string{
Expand Down
5 changes: 3 additions & 2 deletions e2e/common/traits/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,12 @@ func TestBuilderTrait(t *testing.T) {
// Check containers conditions
Eventually(Build(integrationKitNamespace, integrationKitName), TestTimeoutLong).ShouldNot(BeNil())
Eventually(BuildConditions(integrationKitNamespace, integrationKitName), TestTimeoutLong).ShouldNot(BeNil())
Eventually(BuildCondition(integrationKitNamespace, integrationKitName, v1.BuildConditionType("Container custom1 succeeded")), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(
Build(integrationKitNamespace, integrationKitName)().Status.GetCondition(v1.BuildConditionType("Container custom1 succeeded")).Status,
BuildCondition(integrationKitNamespace, integrationKitName, v1.BuildConditionType("Container custom1 succeeded"))().Status,
TestTimeoutShort).Should(Equal(corev1.ConditionFalse))
Eventually(
Build(integrationKitNamespace, integrationKitName)().Status.GetCondition(v1.BuildConditionType("Container custom1 succeeded")).Message,
BuildCondition(integrationKitNamespace, integrationKitName, v1.BuildConditionType("Container custom1 succeeded"))().Message,
TestTimeoutShort).Should(ContainSubstring("No such file or directory"))

Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
Expand Down
10 changes: 10 additions & 0 deletions e2e/support/test_support.go
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,16 @@ func BuildConditions(ns, name string) func() []v1.BuildCondition {
}
}

func BuildCondition(ns string, name string, conditionType v1.BuildConditionType) func() *v1.BuildCondition {
return func() *v1.BuildCondition {
build := Build(ns, name)()
if build != nil && &build.Status != nil && build.Status.Conditions != nil {
return build.Status.GetCondition(conditionType)
}
return &v1.BuildCondition{}
}
}

func BuildFailureRecovery(ns, name string) func() int {
return func() int {
build := Build(ns, name)()
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/camel/v1/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ const (
BuildPhaseInterrupted = "Interrupted"
// BuildPhaseError -- .
BuildPhaseError BuildPhase = "Error"

// BuildConditionScheduled --.
BuildConditionScheduled BuildConditionType = "Scheduled"

// BuildConditionReadyReason --.
BuildConditionReadyReason string = "Ready"
// BuildConditionWaitingReason --.
BuildConditionWaitingReason string = "Waiting"
)

// +genclient
Expand Down
51 changes: 42 additions & 9 deletions pkg/controller/build/build_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"sync"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -31,14 +32,17 @@ import (
"github.com/apache/camel-k/v2/pkg/util/kubernetes"
)

const enqueuedMsg = "%s - the build (%s) gets enqueued"

var runningBuilds sync.Map

type Monitor struct {
maxRunningBuilds int32
buildOrderStrategy v1.BuildOrderStrategy
}

func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Build) (bool, error) {
func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Build) (bool, *v1.BuildCondition, error) {

var runningBuildsTotal int32
runningBuilds.Range(func(_, v interface{}) bool {
runningBuildsTotal++
Expand All @@ -54,24 +58,27 @@ func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Bui
}

if runningBuildsTotal >= bm.maxRunningBuilds {
reason := fmt.Sprintf(
"Maximum number of running builds (%d) exceeded",
runningBuildsTotal,
)
Log.WithValues("request-namespace", requestNamespace, "request-name", requestName, "max-running-builds-limit", runningBuildsTotal).
ForBuild(build).Infof("Maximum number of running builds (%d) exceeded - the build (%s) gets enqueued", runningBuildsTotal, build.Name)

ForBuild(build).Infof(enqueuedMsg, reason, build.Name)
// max number of running builds limit exceeded
return false, nil
return false, scheduledWaitingBuildcondition(build.Name, reason), nil
}

layout := build.Labels[v1.IntegrationKitLayoutLabel]

// Native builds can be run in parallel, as incremental images is not applicable.
if layout == v1.IntegrationKitLayoutNativeSources {
return true, nil
return true, scheduledReadyBuildcondition(build.Name), nil
}

// We assume incremental images is only applicable across images whose layout is identical
withCompatibleLayout, err := labels.NewRequirement(v1.IntegrationKitLayoutLabel, selection.Equals, []string{layout})
if err != nil {
return false, err
return false, nil, err
}

builds := &v1.BuildList{}
Expand All @@ -83,11 +90,12 @@ func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Bui
Selector: labels.NewSelector().Add(*withCompatibleLayout),
})
if err != nil {
return false, err
return false, nil, err
}

var reason string
allowed := true
condition := scheduledReadyBuildcondition(build.Name)
switch bm.buildOrderStrategy {
case v1.BuildOrderStrategyFIFO:
// Check on builds that have been created before the current build and grant precedence if any.
Expand Down Expand Up @@ -116,10 +124,11 @@ func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Bui

if !allowed {
Log.WithValues("request-namespace", requestNamespace, "request-name", requestName, "order-strategy", bm.buildOrderStrategy).
ForBuild(build).Infof("%s - the build (%s) gets enqueued", reason, build.Name)
ForBuild(build).Infof(enqueuedMsg, reason, build.Name)
condition = scheduledWaitingBuildcondition(build.Name, reason)
}

return allowed, nil
return allowed, condition, nil
}

func monitorRunningBuild(build *v1.Build) {
Expand All @@ -129,3 +138,27 @@ func monitorRunningBuild(build *v1.Build) {
func monitorFinishedBuild(build *v1.Build) {
runningBuilds.Delete(types.NamespacedName{Namespace: build.Namespace, Name: build.Name}.String())
}

func scheduledReadyBuildcondition(buildName string) *v1.BuildCondition {
return scheduledBuildcondition(corev1.ConditionTrue, v1.BuildConditionReadyReason, fmt.Sprintf(
"the build (%s) is scheduled",
buildName,
))
}

func scheduledWaitingBuildcondition(buildName string, reason string) *v1.BuildCondition {
return scheduledBuildcondition(corev1.ConditionFalse, v1.BuildConditionWaitingReason, fmt.Sprintf(
enqueuedMsg,
reason,
buildName,
))
}

func scheduledBuildcondition(status corev1.ConditionStatus, reason string, msg string) *v1.BuildCondition {
return &v1.BuildCondition{
Type: v1.BuildConditionScheduled,
Status: status,
Reason: reason,
Message: msg,
}
}
Loading

0 comments on commit 4b38fc1

Please sign in to comment.