diff --git a/api/v1beta1/argo.go b/api/v1beta1/argo.go new file mode 100644 index 0000000..ea1bca9 --- /dev/null +++ b/api/v1beta1/argo.go @@ -0,0 +1,20 @@ +package v1beta1 + +// ArgoHealth is a string type to represent the argo health of a resource +// More info on the argo doc here https://argo-cd.readthedocs.io/en/stable/operator-manual/health/ +type ArgoHealth string + +const ( + // ArgoHealthHealthy the resource is healthy + ArgoHealthHealthy ArgoHealth = "Healthy" + // ArgoHealthProgressing the resource is not healthy yet but still making progress and might be healthy soon + ArgoHealthProgressing ArgoHealth = "Progressing" + // ArgoHealthSuspended the resource is suspended and waiting for some external event to resume (e.g. suspended CronJob or paused Deployment) + ArgoHealthSuspended ArgoHealth = "Suspended" + // ArgoHealthDegraded the resource is degraded + ArgoHealthDegraded ArgoHealth = "Degraded" +) + +type ArgoStatus struct { + Health ArgoHealth `json:"health"` +} diff --git a/api/v1beta1/run_types.go b/api/v1beta1/run_types.go index ac4edf5..b18281f 100644 --- a/api/v1beta1/run_types.go +++ b/api/v1beta1/run_types.go @@ -20,36 +20,46 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // RunSpec defines the desired state of Run type RunSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of Run. Edit run_types.go to remove/update - Foo string `json:"foo,omitempty"` + // StackName is the name of the stack for this run, this is mandatory + // +kubebuilder:validation:MinLength=1 + StackName string `json:"stackName"` } +type RunState string + +const ( + RunStateQueued = "QUEUED" +) + // RunStatus defines the observed state of Run type RunStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + // State is the run state, see RunState for all possibles state of a run + State RunState `json:"state,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" // Run is the Schema for the runs API type Run struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec RunSpec `json:"spec,omitempty"` + Spec RunSpec `json:"spec"` Status RunStatus `json:"status,omitempty"` } +// IsNew return true if the resource has just been created. +// If status.state is nil, it means that the controller does not have handled it yet, so it mean that it's a new one +func (r *Run) IsNew() bool { + return r.Status.State == "" +} + //+kubebuilder:object:root=true // RunList contains a list of Run diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index f14809e..d4990c9 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -24,13 +24,28 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArgoStatus) DeepCopyInto(out *ArgoStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoStatus. +func (in *ArgoStatus) DeepCopy() *ArgoStatus { + if in == nil { + return nil + } + out := new(ArgoStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Run) DeepCopyInto(out *Run) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Run. @@ -101,6 +116,11 @@ func (in *RunSpec) DeepCopy() *RunSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RunStatus) DeepCopyInto(out *RunStatus) { *out = *in + if in.Argo != nil { + in, out := &in.Argo, &out.Argo + *out = new(ArgoStatus) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunStatus. diff --git a/cmd/main.go b/cmd/main.go index 852cf2f..7acec22 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,6 +37,7 @@ import ( appspaceliftiov1beta1 "github.com/spacelift-io/spacelift-operator/api/v1beta1" "github.com/spacelift-io/spacelift-operator/internal/build" "github.com/spacelift-io/spacelift-operator/internal/controller" + "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" ) @@ -104,9 +105,10 @@ func main() { os.Exit(1) } + runRepo := repository.NewRunRepository(mgr.GetClient()) + if err = (&controller.RunReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + RunRepository: runRepo, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Run") os.Exit(1) diff --git a/config/crd/bases/app.spacelift.io_runs.yaml b/config/crd/bases/app.spacelift.io_runs.yaml index 753ce0d..67be935 100644 --- a/config/crd/bases/app.spacelift.io_runs.yaml +++ b/config/crd/bases/app.spacelift.io_runs.yaml @@ -14,7 +14,11 @@ spec: singular: run scope: Namespaced versions: - - name: v1beta1 + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + name: v1beta1 schema: openAPIV3Schema: description: Run is the Schema for the runs API @@ -34,14 +38,35 @@ spec: spec: description: RunSpec defines the desired state of Run properties: - foo: - description: Foo is an example field of Run. Edit run_types.go to - remove/update + stackName: + description: StackName is the name of the stack for this run, this + is mandatory + minLength: 1 type: string + required: + - stackName type: object status: description: RunStatus defines the observed state of Run + properties: + argo: + description: Argo is a status that could be used by argo health check + to sync on health + properties: + health: + description: ArgoHealth is a string type to represent the argo + health of a resource More info on the argo doc here https://argo-cd.readthedocs.io/en/stable/operator-manual/health/ + type: string + required: + - health + type: object + state: + description: State is the run state, see RunState for all possibles + state of a run + type: string type: object + required: + - spec type: object served: true storage: true diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index a25d718..b5d33a8 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -36,35 +36,24 @@ spec: labels: control-plane: controller-manager spec: - # TODO(user): Uncomment the following code to configure the nodeAffinity expression - # according to the platforms which are supported by your solution. - # It is considered best practice to support multiple architectures. You can - # build your manager image using the makefile target docker-buildx. - # affinity: - # nodeAffinity: - # requiredDuringSchedulingIgnoredDuringExecution: - # nodeSelectorTerms: - # - matchExpressions: - # - key: kubernetes.io/arch - # operator: In - # values: - # - amd64 - # - arm64 - # - ppc64le - # - s390x - # - key: kubernetes.io/os - # operator: In - # values: - # - linux + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - key: kubernetes.io/os + operator: In + values: + - linux securityContext: runAsNonRoot: true - # TODO(user): For common cases that do not require escalating privileges - # it is recommended to ensure that all your Pods/Containers are restrictive. - # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted - # Please uncomment the following code if your project does NOT have to work on old Kubernetes - # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). - # seccompProfile: - # type: RuntimeDefault + seccompProfile: + type: RuntimeDefault containers: - command: - /manager @@ -89,14 +78,11 @@ spec: port: 8081 initialDelaySeconds: 5 periodSeconds: 10 - # TODO(user): Configure the resources accordingly based on the project requirements. - # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: - cpu: 500m memory: 128Mi requests: - cpu: 10m - memory: 64Mi + cpu: 100m + memory: 128Mi serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 diff --git a/config/samples/_v1beta1_run.yaml b/config/samples/_v1beta1_run.yaml index 359c2f5..48612d9 100644 --- a/config/samples/_v1beta1_run.yaml +++ b/config/samples/_v1beta1_run.yaml @@ -9,4 +9,4 @@ metadata: app.kubernetes.io/created-by: spacelift-operator name: run-sample spec: - # TODO(user): Add fields here + stackName: stack-sample diff --git a/go.mod b/go.mod index 930a7e0..557958f 100644 --- a/go.mod +++ b/go.mod @@ -52,13 +52,15 @@ 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/testify v1.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.13.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.3 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 7f959c9..678dbc3 100644 --- a/go.sum +++ b/go.sum @@ -155,8 +155,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -199,8 +199,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -216,8 +216,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= -golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -225,8 +225,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -242,13 +242,13 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/controller/run_controller.go b/internal/controller/run_controller.go index 899b7fa..683a120 100644 --- a/internal/controller/run_controller.go +++ b/internal/controller/run_controller.go @@ -18,19 +18,23 @@ package controller import ( "context" + "reflect" + "time" - "k8s.io/apimachinery/pkg/runtime" + 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" - appspaceliftiov1beta1 "github.com/spacelift-io/spacelift-operator/api/v1beta1" + "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" ) // RunReconciler reconciles a Run object type RunReconciler struct { - client.Client - Scheme *runtime.Scheme + RunRepository *repository.RunRepository } //+kubebuilder:rbac:groups=app.spacelift.io,resources=runs,verbs=get;list;watch;create;update;patch;delete @@ -39,24 +43,66 @@ type RunReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the Run object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.0/pkg/reconcile func (r *RunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + logger := log.FromContext(ctx) - // TODO(user): your logic here + run, err := r.RunRepository.Get(ctx, req.NamespacedName) + // The Run is removed, this should not happen because we filter out deletion events. + // This can't really hurt and makes the reconciliation logic a bit more straightforward to read + if err != nil && k8sErrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + if err != nil { + logger.Error(err, "Unable to retrieve Run from kube API.") + return ctrl.Result{}, 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 +} + +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} + if err := r.RunRepository.UpdateStatus(ctx, run); err != nil { + if k8sErrors.IsConflict(err) { + logger.Info("Conflict on Run status update, let's try again.") + return ctrl.Result{RequeueAfter: time.Second * 3}, nil + } + return ctrl.Result{}, err + } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *RunReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&appspaceliftiov1beta1.Run{}). + For(&v1beta1.Run{}). + WithEventFilter(predicate.Funcs{ + // Always handle new resource creation + CreateFunc: func(event.CreateEvent) bool { return true }, + // Let's consider run immutables and only care about update on the status + UpdateFunc: func(e event.UpdateEvent) bool { + oldRun, _ := e.ObjectOld.(*v1beta1.Run) + newRun, _ := e.ObjectNew.(*v1beta1.Run) + return !reflect.DeepEqual(oldRun.Status, newRun.Status) + }, + // We don't care about run removal + DeleteFunc: func(event.DeleteEvent) bool { return false }, + }). Complete(r) } diff --git a/internal/k8s/repository/run_repository.go b/internal/k8s/repository/run_repository.go new file mode 100644 index 0000000..6a94572 --- /dev/null +++ b/internal/k8s/repository/run_repository.go @@ -0,0 +1,30 @@ +package repository + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/spacelift-io/spacelift-operator/api/v1beta1" +) + +type RunRepository struct { + client client.Client +} + +func NewRunRepository(client client.Client) *RunRepository { + return &RunRepository{client: client} +} + +func (r *RunRepository) Get(ctx context.Context, name types.NamespacedName) (*v1beta1.Run, error) { + var run v1beta1.Run + if err := r.client.Get(ctx, name, &run); err != nil { + return nil, err + } + return &run, nil +} + +func (r *RunRepository) UpdateStatus(ctx context.Context, run *v1beta1.Run) error { + return r.client.Status().Update(ctx, run) +} diff --git a/internal/logging/keys.go b/internal/logging/keys.go new file mode 100644 index 0000000..a8e0d23 --- /dev/null +++ b/internal/logging/keys.go @@ -0,0 +1,5 @@ +package logging + +const ( + ArgoHealth = "argo.health" +)