Skip to content

Commit

Permalink
feat: add run creation and poll for status
Browse files Browse the repository at this point in the history
  • Loading branch information
eliecharra committed Mar 5, 2024
1 parent 6cbbe6d commit 519fb7a
Show file tree
Hide file tree
Showing 16 changed files with 730 additions and 35 deletions.
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
generate: gogen controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."

.PHONY: fmt
Expand All @@ -62,7 +62,7 @@ vet: ## Run go vet against code.

.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -v -race -coverprofile cover.out

##@ Build

Expand Down Expand Up @@ -171,6 +171,10 @@ $(ENVTEST): $(LOCALBIN)
lint: ## Run golangci-lint
golangci-lint run

.PHONY: gogen
gogen: install-go-tools
go generate ./...

.PHONY: install-go-tools ## Install dev tools
install-go-tools:
go generate -tags tools
49 changes: 48 additions & 1 deletion api/v1beta1/run_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,39 @@ type RunSpec struct {
type RunState string

const (
RunStateQueued = "QUEUED"
RunStateQueued RunState = "QUEUED"
RunStateCanceled RunState = "CANCELED"
RunStateFailed RunState = "FAILED"
RunStateFinished RunState = "FINISHED"
RunStateUnconfirmed RunState = "UNCONFIRMED"
RunStateDiscarded RunState = "DISCARDED"
RunStateStopped RunState = "STOPPED"
RunStateSkipped RunState = "SKIPPED"
)

var terminalStates = map[RunState]interface{}{
RunStateCanceled: nil,
RunStateFailed: nil,
RunStateFinished: nil,
RunStateDiscarded: nil,
RunStateStopped: nil,
RunStateSkipped: nil,
}

// RunStatus defines the observed state of Run
type RunStatus struct {
// State is the run state, see RunState for all possibles state of a run
State RunState `json:"state,omitempty"`
// Id is the run ULID on Spacelift
Id string `json:"id,omitempty"`
// Argo is a status that could be used by argo health check to sync on health
Argo *ArgoStatus `json:"argo,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="State",type=string,JSONPath=".status.state"
//+kubebuilder:printcolumn:name="Id",type=string,JSONPath=".status.id"

// Run is the Schema for the runs API
type Run struct {
Expand All @@ -60,6 +79,34 @@ func (r *Run) IsNew() bool {
return r.Status.State == ""
}

// IsTerminated returns true if the run is in a terminal state
func (r *Run) IsTerminated() bool {
_, found := terminalStates[r.Status.State]
return found
}

// SetState set status.state and also update the argo health
func (r *Run) SetState(state RunState) {
r.Status.State = state
argoHealth := &ArgoStatus{
Health: ArgoHealthProgressing,
}
if r.Status.State == RunStateFinished ||
r.Status.State == RunStateSkipped {
argoHealth.Health = ArgoHealthHealthy
}
if r.Status.State == RunStateUnconfirmed {
argoHealth.Health = ArgoHealthSuspended
}
if r.Status.State == RunStateFailed ||
r.Status.State == RunStateStopped ||
r.Status.State == RunStateCanceled ||
r.Status.State == RunStateDiscarded {
argoHealth.Health = ArgoHealthDegraded
}
r.Status.Argo = argoHealth
}

//+kubebuilder:object:root=true

// RunList contains a list of Run
Expand Down
8 changes: 7 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import (
"github.com/spacelift-io/spacelift-operator/internal/k8s/repository"
"github.com/spacelift-io/spacelift-operator/internal/logging"
"github.com/spacelift-io/spacelift-operator/internal/logging/encoders"
spaceliftRepository "github.com/spacelift-io/spacelift-operator/internal/spacelift/repository"
"github.com/spacelift-io/spacelift-operator/internal/spacelift/watcher"
)

var (
Expand Down Expand Up @@ -106,9 +108,13 @@ func main() {
}

runRepo := repository.NewRunRepository(mgr.GetClient())
spaceliftRunRepo := spaceliftRepository.NewRunRepository(mgr.GetClient())
runWatcher := watcher.NewRunWatcher(runRepo, spaceliftRunRepo)

if err = (&controller.RunReconciler{
RunRepository: runRepo,
RunRepository: runRepo,
SpaceliftRunRepository: spaceliftRunRepo,
RunWatcher: runWatcher,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Run")
os.Exit(1)
Expand Down
6 changes: 6 additions & 0 deletions config/crd/bases/app.spacelift.io_runs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ spec:
- jsonPath: .status.state
name: State
type: string
- jsonPath: .status.id
name: Id
type: string
name: v1beta1
schema:
openAPIV3Schema:
Expand Down Expand Up @@ -60,6 +63,9 @@ spec:
required:
- health
type: object
id:
description: Id is the run ULID on Spacelift
type: string
state:
description: State is the run state, see RunState for all possibles
state of a run
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.21.4
require (
github.com/fatih/color v1.16.0
github.com/nwidger/jsoncolor v0.3.2
github.com/oklog/ulid/v2 v2.1.0
github.com/pkg/errors v0.9.1
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
github.com/spacelift-io/spacectl v0.30.0
Expand Down Expand Up @@ -55,6 +56,7 @@ require (
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/net v0.18.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,13 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMhHQ=
github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down Expand Up @@ -152,6 +155,7 @@ github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ai
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
54 changes: 44 additions & 10 deletions internal/controller/run_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ import (

k8sErrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"

"github.com/spacelift-io/spacelift-operator/api/v1beta1"
"github.com/spacelift-io/spacelift-operator/internal/k8s/repository"
"github.com/spacelift-io/spacelift-operator/internal/logging"
spaceliftRepository "github.com/spacelift-io/spacelift-operator/internal/spacelift/repository"
"github.com/spacelift-io/spacelift-operator/internal/spacelift/watcher"
)

// RunReconciler reconciles a Run object
type RunReconciler struct {
RunRepository *repository.RunRepository
RunRepository *repository.RunRepository
SpaceliftRunRepository spaceliftRepository.RunRepository
RunWatcher *watcher.RunWatcher
}

//+kubebuilder:rbac:groups=app.spacelift.io,resources=runs,verbs=get;list;watch;create;update;patch;delete
Expand All @@ -58,26 +63,31 @@ func (r *RunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
}
if err != nil {
logger.Error(err, "Unable to retrieve Run from kube API.")
return ctrl.Result{}, err
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// If the run is new, then create it on spacelift and update the status
if run.IsNew() {
return r.handleNewRun(ctx, run)
}

logger.Info("Run updated", logging.ArgoHealth, run.Status.Argo.Health)

return ctrl.Result{}, nil
return r.handleRunUpdate(ctx, run)
}

func (r *RunReconciler) handleNewRun(ctx context.Context, run *v1beta1.Run) (ctrl.Result, error) {
logger := log.FromContext(ctx)
logger.Info("New run created")
// TODO(eliecharra): Check that the stack exists based on spec.stackName
// TODO(eliecharra): Create the run on spacelift
run.Status.State = v1beta1.RunStateQueued
run.Status.Argo = &v1beta1.ArgoStatus{Health: v1beta1.ArgoHealthProgressing}
spaceliftRun, err := r.SpaceliftRunRepository.Create(ctx, run)
if err != nil {
logger.Error(err, "Unable to create the run in spacelift")
// TODO(eliecharra): Implement better error handling and retry errors that could be retried
return ctrl.Result{}, nil
}
run.SetState(v1beta1.RunState(spaceliftRun.State))
run.Status.Id = spaceliftRun.RunID
logger.WithValues(
logging.RunState, run.Status.State,
logging.RunId, run.Status.Id,
).Info("New run created")
if err := r.RunRepository.UpdateStatus(ctx, run); err != nil {
if k8sErrors.IsConflict(err) {
logger.Info("Conflict on Run status update, let's try again.")
Expand All @@ -88,6 +98,30 @@ func (r *RunReconciler) handleNewRun(ctx context.Context, run *v1beta1.Run) (ctr
return ctrl.Result{}, nil
}

func (r *RunReconciler) handleRunUpdate(ctx context.Context, run *v1beta1.Run) (ctrl.Result, error) {
logger := log.FromContext(ctx)

// If a run is not terminated and not watched it probably mean that
// - a new run has been created
// - the controller has crashed and is restarting
// In that case we start a watcher on the run
if !run.IsTerminated() && !r.RunWatcher.IsWatched(run) {
if err := r.RunWatcher.Start(ctx, run); err != nil {
logger.Error(err, "Cannot start run watcher")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}

logger.Info("Run updated",
logging.RunId, run.Status.Id,
logging.RunState, run.Status.State,
logging.ArgoHealth, run.Status.Argo.Health,
)

return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *RunReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Expand Down
Loading

0 comments on commit 519fb7a

Please sign in to comment.