diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index 252b5c39..285eac51 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -17,6 +17,8 @@ limitations under the License. package v1alpha1 import ( + "errors" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -75,6 +77,19 @@ type RunnerSpec struct { TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"` } +// ValidateRepository validates repository field. +func (rs *RunnerSpec) ValidateRepository() error { + // Organization and repository are both exclusive. + if len(rs.Organization) == 0 && len(rs.Repository) == 0 { + return errors.New("Spec needs organization or repository") + } + if len(rs.Organization) > 0 && len(rs.Repository) > 0 { + return errors.New("Spec cannot have both organization and repository") + } + + return nil +} + // RunnerStatus defines the observed state of Runner type RunnerStatus struct { Registration RunnerStatusRegistration `json:"registration"` diff --git a/api/v1alpha1/runner_webhook.go b/api/v1alpha1/runner_webhook.go new file mode 100644 index 00000000..59fe0e8e --- /dev/null +++ b/api/v1alpha1/runner_webhook.go @@ -0,0 +1,84 @@ +/* +Copyright 2020 The actions-runner-controller authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var runnerLog = logf.Log.WithName("runner-resource") + +func (r *Runner) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=mutate.runner.actions.summerwind.dev + +var _ webhook.Defaulter = &Runner{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *Runner) Default() { + // Nothing to do. +} + +// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=validate.runner.actions.summerwind.dev + +var _ webhook.Validator = &Runner{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Runner) ValidateCreate() error { + runnerLog.Info("validate resource to be created", "name", r.Name) + return r.Validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Runner) ValidateUpdate(old runtime.Object) error { + runnerLog.Info("validate resource to be updated", "name", r.Name) + return r.Validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Runner) ValidateDelete() error { + return nil +} + +// Validate validates resource spec. +func (r *Runner) Validate() error { + var ( + errList field.ErrorList + err error + ) + + err = r.Spec.ValidateRepository() + if err != nil { + errList = append(errList, field.Invalid(field.NewPath("spec", "repository"), r.Spec.Repository, err.Error())) + } + + if len(errList) > 0 { + return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList) + } + + return nil +} diff --git a/api/v1alpha1/runnerdeployment_webhook.go b/api/v1alpha1/runnerdeployment_webhook.go new file mode 100644 index 00000000..d64e6acb --- /dev/null +++ b/api/v1alpha1/runnerdeployment_webhook.go @@ -0,0 +1,84 @@ +/* +Copyright 2020 The actions-runner-controller authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var runenrDeploymentLog = logf.Log.WithName("runnerdeployment-resource") + +func (r *RunnerDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=mutate.runnerdeployment.actions.summerwind.dev + +var _ webhook.Defaulter = &RunnerDeployment{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *RunnerDeployment) Default() { + // Nothing to do. +} + +// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=validate.runnerdeployment.actions.summerwind.dev + +var _ webhook.Validator = &RunnerDeployment{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerDeployment) ValidateCreate() error { + runenrDeploymentLog.Info("validate resource to be created", "name", r.Name) + return r.Validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerDeployment) ValidateUpdate(old runtime.Object) error { + runenrDeploymentLog.Info("validate resource to be updated", "name", r.Name) + return r.Validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerDeployment) ValidateDelete() error { + return nil +} + +// Validate validates resource spec. +func (r *RunnerDeployment) Validate() error { + var ( + errList field.ErrorList + err error + ) + + err = r.Spec.Template.Spec.ValidateRepository() + if err != nil { + errList = append(errList, field.Invalid(field.NewPath("spec", "template", "spec", "repository"), r.Spec.Template.Spec.Repository, err.Error())) + } + + if len(errList) > 0 { + return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList) + } + + return nil +} diff --git a/api/v1alpha1/runnerreplicaset_webhook.go b/api/v1alpha1/runnerreplicaset_webhook.go new file mode 100644 index 00000000..b026ff6d --- /dev/null +++ b/api/v1alpha1/runnerreplicaset_webhook.go @@ -0,0 +1,84 @@ +/* +Copyright 2020 The actions-runner-controller authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var runnerReplicaSetLog = logf.Log.WithName("runnerreplicaset-resource") + +func (r *RunnerReplicaSet) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=mutate.runnerreplicaset.actions.summerwind.dev + +var _ webhook.Defaulter = &RunnerReplicaSet{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *RunnerReplicaSet) Default() { + // Nothing to do. +} + +// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=validate.runnerreplicaset.actions.summerwind.dev + +var _ webhook.Validator = &RunnerReplicaSet{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerReplicaSet) ValidateCreate() error { + runnerReplicaSetLog.Info("validate resource to be created", "name", r.Name) + return r.Validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerReplicaSet) ValidateUpdate(old runtime.Object) error { + runnerReplicaSetLog.Info("validate resource to be updated", "name", r.Name) + return r.Validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *RunnerReplicaSet) ValidateDelete() error { + return nil +} + +// Validate validates resource spec. +func (r *RunnerReplicaSet) Validate() error { + var ( + errList field.ErrorList + err error + ) + + err = r.Spec.Template.Spec.ValidateRepository() + if err != nil { + errList = append(errList, field.Invalid(field.NewPath("spec", "template", "spec", "repository"), r.Spec.Template.Spec.Repository, err.Error())) + } + + if len(errList) > 0 { + return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList) + } + + return nil +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 029f7219..7989c3da 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1alpha1 import ( "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index 6babdd23..f69fafb8 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -66,7 +66,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, client.IgnoreNotFound(err) } - err := validateRunnerSpec(&runner.Spec) + err := runner.Validate() if err != nil { log.Info("Failed to validate runner spec", "error", err.Error()) return ctrl.Result{}, nil @@ -88,7 +88,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } } else { finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers) - + if removed { if len(runner.Status.Registration.Token) > 0 { ok, err := r.unregisterRunner(ctx, runner.Spec.Organization, runner.Spec.Repository, runner.Name) @@ -449,15 +449,3 @@ func removeFinalizer(finalizers []string) ([]string, bool) { return result, removed } - -// organization & repository are both exclusive - however this cannot be checked with kubebuilder -// therefore have an additional check here to log an error in case spec is invalid -func validateRunnerSpec(spec *v1alpha1.RunnerSpec) error { - if len(spec.Organization) == 0 && len(spec.Repository) == 0 { - return fmt.Errorf("RunnerSpec needs organization or repository") - } - if len(spec.Organization) > 0 && len(spec.Repository) > 0 { - return fmt.Errorf("RunnerSpec cannot have both organization and repository") - } - return nil -} diff --git a/main.go b/main.go index 8e7d1327..295601a3 100644 --- a/main.go +++ b/main.go @@ -174,6 +174,19 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "RunnerDeployment") os.Exit(1) } + + if err = (&actionsv1alpha1.Runner{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Runner") + os.Exit(1) + } + if err = (&actionsv1alpha1.RunnerDeployment{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "RunnerDeployment") + os.Exit(1) + } + if err = (&actionsv1alpha1.RunnerReplicaSet{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "RunnerReplicaSet") + os.Exit(1) + } // +kubebuilder:scaffold:builder setupLog.Info("starting manager")