diff --git a/PROJECT b/PROJECT index d45a60a..0ed4cbb 100644 --- a/PROJECT +++ b/PROJECT @@ -30,4 +30,13 @@ resources: kind: ClusterTunnel path: github.com/adyanth/cloudflare-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: cfargotunnel.com + group: networking + kind: AccessService + path: github.com/adyanth/cloudflare-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/accessservice_types.go b/api/v1alpha1/accessservice_types.go new file mode 100644 index 0000000..4ac6673 --- /dev/null +++ b/api/v1alpha1/accessservice_types.go @@ -0,0 +1,76 @@ +/* +Copyright 2022. + +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AccessServiceSpec defines the desired state of AccessService +type AccessServiceSpec struct { + // FQDN to connect to for the TCP tunnel + //+kubebuilder:validation:Required + Hostname string `json:"hostname"` + + // Protocol defines the protocol to use, only TCP for now, default + //+kubebuilder:validation:Enum:="tcp";"udp" + //+kubebuilder:default="tcp" + Protocol string `json:"protocol"` + + // Port defines the port for the service to listen on + //+kubebuilder:validation:Minimum:=1 + //+kubebuilder:validation:Maximum:=65535 + Port int32 `json:"port"` + + // ServiceName defines the name of the service for this port to be exposed on + //+kubebuilder:validation:Required + ServiceName string `json:"serviceName"` + + // Replicas defines the number of cloudflared access replicas to run + Replicas int32 `json:"replicas"` +} + +// AccessServiceStatus defines the observed state of AccessService +type AccessServiceStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// AccessService is the Schema for the accessservices API +type AccessService struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AccessServiceSpec `json:"spec,omitempty"` + Status AccessServiceStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AccessServiceList contains a list of AccessService +type AccessServiceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AccessService `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AccessService{}, &AccessServiceList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 153967f..4cf7a81 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,95 @@ 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 *AccessService) DeepCopyInto(out *AccessService) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessService. +func (in *AccessService) DeepCopy() *AccessService { + if in == nil { + return nil + } + out := new(AccessService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AccessService) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessServiceList) DeepCopyInto(out *AccessServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AccessService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessServiceList. +func (in *AccessServiceList) DeepCopy() *AccessServiceList { + if in == nil { + return nil + } + out := new(AccessServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AccessServiceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessServiceSpec) DeepCopyInto(out *AccessServiceSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessServiceSpec. +func (in *AccessServiceSpec) DeepCopy() *AccessServiceSpec { + if in == nil { + return nil + } + out := new(AccessServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessServiceStatus) DeepCopyInto(out *AccessServiceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessServiceStatus. +func (in *AccessServiceStatus) DeepCopy() *AccessServiceStatus { + if in == nil { + return nil + } + out := new(AccessServiceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CloudflareDetails) DeepCopyInto(out *CloudflareDetails) { *out = *in diff --git a/config/crd/bases/networking.cfargotunnel.com_accessservices.yaml b/config/crd/bases/networking.cfargotunnel.com_accessservices.yaml new file mode 100644 index 0000000..8d342d4 --- /dev/null +++ b/config/crd/bases/networking.cfargotunnel.com_accessservices.yaml @@ -0,0 +1,85 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: accessservices.networking.cfargotunnel.com +spec: + group: networking.cfargotunnel.com + names: + kind: AccessService + listKind: AccessServiceList + plural: accessservices + singular: accessservice + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: AccessService is the Schema for the accessservices API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AccessServiceSpec defines the desired state of AccessService + properties: + hostname: + description: FQDN to connect to for the TCP tunnel + type: string + port: + description: Port defines the port for the service to listen on + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + default: tcp + description: Protocol defines the protocol to use, only TCP for now, + default + enum: + - tcp + - udp + type: string + replicas: + description: Replicas defines the number of cloudflared access replicas + to run + format: int32 + type: integer + serviceName: + description: ServiceName defines the name of the service for this + port to be exposed on + type: string + required: + - hostname + - port + - protocol + - replicas + - serviceName + type: object + status: + description: AccessServiceStatus defines the observed state of AccessService + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index c5325ef..e62cc58 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/networking.cfargotunnel.com_tunnels.yaml - bases/networking.cfargotunnel.com_clustertunnels.yaml - bases/networking.cfargotunnel.com_tunnelbindings.yaml +- bases/networking.cfargotunnel.com_accessservices.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -13,6 +14,7 @@ patchesStrategicMerge: #- patches/webhook_in_tunnels.yaml #- patches/webhook_in_clustertunnels.yaml #- patches/webhook_in_tunnelbindings.yaml +#- patches/webhook_in_accessservices.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -20,6 +22,7 @@ patchesStrategicMerge: #- patches/cainjection_in_tunnels.yaml #- patches/cainjection_in_clustertunnels.yaml #- patches/cainjection_in_tunnelbindings.yaml +#- patches/cainjection_in_accessservices.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_accessservices.yaml b/config/crd/patches/cainjection_in_accessservices.yaml new file mode 100644 index 0000000..3248b0a --- /dev/null +++ b/config/crd/patches/cainjection_in_accessservices.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: accessservices.networking.cfargotunnel.com diff --git a/config/crd/patches/webhook_in_accessservices.yaml b/config/crd/patches/webhook_in_accessservices.yaml new file mode 100644 index 0000000..e098118 --- /dev/null +++ b/config/crd/patches/webhook_in_accessservices.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: accessservices.networking.cfargotunnel.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/accessservice_editor_role.yaml b/config/rbac/accessservice_editor_role.yaml new file mode 100644 index 0000000..42150ae --- /dev/null +++ b/config/rbac/accessservice_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit accessservices. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: accessservice-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: cloudflare-operator + app.kubernetes.io/part-of: cloudflare-operator + app.kubernetes.io/managed-by: kustomize + name: accessservice-editor-role +rules: +- apiGroups: + - networking.cfargotunnel.com + resources: + - accessservices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.cfargotunnel.com + resources: + - accessservices/status + verbs: + - get diff --git a/config/rbac/accessservice_viewer_role.yaml b/config/rbac/accessservice_viewer_role.yaml new file mode 100644 index 0000000..1f4f9e1 --- /dev/null +++ b/config/rbac/accessservice_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view accessservices. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: accessservice-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: cloudflare-operator + app.kubernetes.io/part-of: cloudflare-operator + app.kubernetes.io/managed-by: kustomize + name: accessservice-viewer-role +rules: +- apiGroups: + - networking.cfargotunnel.com + resources: + - accessservices + verbs: + - get + - list + - watch +- apiGroups: + - networking.cfargotunnel.com + resources: + - accessservices/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e58a5ce..fff2238 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -54,9 +54,39 @@ rules: resources: - services verbs: + - create + - delete - get - list + - patch + - update - watch +- apiGroups: + - networking.cfargotunnel.com + resources: + - accessservices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.cfargotunnel.com + resources: + - accessservices/finalizers + verbs: + - update +- apiGroups: + - networking.cfargotunnel.com + resources: + - accessservices/status + verbs: + - get + - patch + - update - apiGroups: - networking.cfargotunnel.com resources: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index e7477b7..1813e32 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -7,4 +7,5 @@ resources: - v1_service.yaml - networking_v1alpha1_clustertunnel.yaml - networking_v1alpha1_tunnelbinding.yaml +- networking_v1alpha1_accessservice.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/networking_v1alpha1_accessservice.yaml b/config/samples/networking_v1alpha1_accessservice.yaml new file mode 100644 index 0000000..cd785cd --- /dev/null +++ b/config/samples/networking_v1alpha1_accessservice.yaml @@ -0,0 +1,12 @@ +apiVersion: networking.cfargotunnel.com/v1alpha1 +kind: AccessService +metadata: + labels: + app.kubernetes.io/name: accessservice + app.kubernetes.io/instance: accessservice-sample + app.kubernetes.io/part-of: cloudflare-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: cloudflare-operator + name: accessservice-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/accessservice_controller.go b/controllers/accessservice_controller.go new file mode 100644 index 0000000..4f67070 --- /dev/null +++ b/controllers/accessservice_controller.go @@ -0,0 +1,258 @@ +/* +Copyright 2022. + +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 controllers + +import ( + "context" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + apitypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + networkingv1alpha1 "github.com/adyanth/cloudflare-operator/api/v1alpha1" + "github.com/go-logr/logr" +) + +// AccessServiceReconciler reconciles a AccessService object +type AccessServiceReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + + access *networkingv1alpha1.AccessService + log logr.Logger +} + +//+kubebuilder:rbac:groups=networking.cfargotunnel.com,resources=accessservices,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=networking.cfargotunnel.com,resources=accessservices/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=networking.cfargotunnel.com,resources=accessservices/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch + +// 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 AccessService 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.13.0/pkg/reconcile +func (r *AccessServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.log = log.FromContext(ctx) + + // Lookup the AccessService resource + r.access = &networkingv1alpha1.AccessService{} + if err := r.Get(ctx, req.NamespacedName, r.access); err != nil { + if apierrors.IsNotFound(err) { + // AccessService object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + r.log.Info("AccessService deleted, nothing to do") + return ctrl.Result{}, nil + } + r.log.Error(err, "unable to fetch AccessService") + return ctrl.Result{}, err + } + + if res, err := r.createAccessService(ctx); err != nil || (res != ctrl.Result{}) { + return res, err + } + + if res, err := r.createAccessDeployment(ctx); err != nil || (res != ctrl.Result{}) { + return res, err + } + + return ctrl.Result{}, nil +} + +func (r *AccessServiceReconciler) createAccessService(ctx context.Context) (ctrl.Result, error) { + acService := &corev1.Service{} + acServiceName := apitypes.NamespacedName{Name: r.access.Spec.ServiceName, Namespace: r.access.Namespace} + if err := r.Client.Get(ctx, acServiceName, acService); err != nil && apierrors.IsNotFound(err) { + // Define a new service + svc := r.serviceForAccess() + r.log.Info("Creating a new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name) + r.Recorder.Event(r.access, corev1.EventTypeNormal, "Deploying", "Creating AccessService Service") + err = r.Client.Create(ctx, svc) + if err != nil { + r.log.Error(err, "Failed to create new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name) + r.Recorder.Event(r.access, corev1.EventTypeWarning, "FailedDeploying", "Creating AccessService Service failed") + return ctrl.Result{}, err + } + r.log.Info("Service created", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name) + r.Recorder.Event(r.access, corev1.EventTypeNormal, "Deployed", "Created AccessService Service") + return ctrl.Result{Requeue: true}, nil + } else if err != nil { + r.log.Error(err, "Failed to get Service") + r.Recorder.Event(r.access, corev1.EventTypeWarning, "FailedDeployed", "Reading AccessService Service failed") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +func (r *AccessServiceReconciler) getLabels() map[string]string { + return map[string]string{ + "app": "cloudflared", + "access": r.access.Name, + "service": r.access.Spec.ServiceName, + "protocol": r.access.Spec.Protocol, + "port": fmt.Sprintf("%d", r.access.Spec.Port), + } +} + +// deploymentForService returns a service object +func (r *AccessServiceReconciler) serviceForAccess() *corev1.Service { + ls := r.getLabels() + + if r.access.Spec.Protocol != "TCP" { + r.log.Error( + fmt.Errorf("ignoring Protocol, using TCP"), + "Ignoring Protocol, using TCP", + "AccessService.Name", r.access.Name, + "AccessService.Namespace", r.access.Namespace, + "protocol", r.access.Spec.Protocol, + ) + } + proto := corev1.ProtocolTCP + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.access.Spec.ServiceName, + Namespace: r.access.Namespace, + Labels: ls, + }, + Spec: corev1.ServiceSpec{ + Selector: ls, + Ports: []corev1.ServicePort{ + { + Port: r.access.Spec.Port, + TargetPort: intstr.FromString("access"), + Protocol: proto, + }, + }, + }, + } + // Set AccessService instance as the owner and controller + ctrl.SetControllerReference(r.access, svc, r.Scheme) + return svc +} + +func (r *AccessServiceReconciler) createAccessDeployment(ctx context.Context) (ctrl.Result, error) { + acDeploy := &appsv1.Deployment{} + acDeployName := apitypes.NamespacedName{Name: r.access.Spec.ServiceName, Namespace: r.access.Namespace} + if err := r.Client.Get(ctx, acDeployName, acDeploy); err != nil && apierrors.IsNotFound(err) { + // Define a new deployment + dep := r.deploymentForAccess() + r.log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + r.Recorder.Event(r.access, corev1.EventTypeNormal, "Deploying", "Creating AccessService Deployment") + err = r.Client.Create(ctx, dep) + if err != nil { + r.log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + r.Recorder.Event(r.access, corev1.EventTypeWarning, "FailedDeploying", "Creating AccessService Deployment failed") + return ctrl.Result{}, err + } + r.log.Info("Deployment created", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + r.Recorder.Event(r.access, corev1.EventTypeNormal, "Deployed", "Created AccessService Deployment") + return ctrl.Result{Requeue: true}, nil + } else if err != nil { + r.log.Error(err, "Failed to get Deployment") + r.Recorder.Event(r.access, corev1.EventTypeWarning, "FailedDeployed", "Reading AccessService Deployment failed") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +// deploymentForAccess returns a deployment object +func (r *AccessServiceReconciler) deploymentForAccess() *appsv1.Deployment { + replicas := r.access.Spec.Replicas + ls := r.getLabels() + + if r.access.Spec.Protocol != "TCP" { + r.log.Error( + fmt.Errorf("ignoring Protocol, using TCP"), + "Ignoring Protocol, using TCP", + "AccessService.Name", r.access.Name, + "AccessService.Namespace", r.access.Namespace, + "protocol", r.access.Spec.Protocol, + ) + } + proto := corev1.ProtocolTCP + + url := fmt.Sprintf("0.0.0.0:%d", r.access.Spec.Port) + args := []string{"access", r.access.Spec.Protocol, "--hostname", r.access.Spec.Hostname, "--url", url} + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.access.Spec.ServiceName, + Namespace: r.access.Namespace, + Labels: ls, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Image: "cloudflare/cloudflared:latest", + Name: "cloudflared", + Args: args, + Ports: []corev1.ContainerPort{ + { + Name: "access", + ContainerPort: r.access.Spec.Port, + Protocol: proto, + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{"memory": resource.MustParse("30Mi"), "cpu": resource.MustParse("10m")}, + Limits: corev1.ResourceList{"memory": resource.MustParse("256Mi")}, + }, + }}, + }, + }, + }, + } + // Set AccessService instance as the owner and controller + ctrl.SetControllerReference(r.access, dep, r.Scheme) + return dep +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AccessServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Recorder = mgr.GetEventRecorderFor("cloudflare-operator") + return ctrl.NewControllerManagedBy(mgr). + For(&networkingv1alpha1.AccessService{}). + Owns(&corev1.Service{}). + Owns(&appsv1.Deployment{}). + Complete(r) +} diff --git a/docs/configuration.md b/docs/configuration.md index aaba265..00d3424 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,6 +8,7 @@ The operator iself accepts command line arguments to override some of the defaul | **Command line argument** | **Type** | **Description** | **Default Value** | | |--------------------------------|----------|------------------------------------------------------------------------------------------------------------|----------------------------|---| +| `--default-image` | string | The default cloudflared image to use for tunnels | | | | `--cluster-resource-namespace` | string | The default namespace for cluster scoped resources | cloudflare-operator-system | | | `--overwrite-unmanaged-dns` | boolean | Overwrite existing DNS records that do not have a corresponding managed TXT record | false | | | `--leader-elect` | boolean | Enable leader election for controller manager, this is optional for operator running with a single replica | true | | @@ -58,7 +59,9 @@ spec: # cloudflared configuration fallbackTarget: http_status:404 # The default service to point cloudflared to. Defaults to http_status:404 - image: cloudflare/cloudflared:2022.3.1 # Image to run. Used for running an up-to-date image. Can be swapped out to an arm based image if needed + podSpec: # Overwrite any fields of the generated pod spec for cloudflared + containers: + - image: cloudflared:latest # Overwrite image to be used noTlsVerify: false # Disables the TLS verification to backend services globally originCaPool: homelab-ca # Secret containing CA certificates to trust. Must contain tls.crt to be trusted globally and optionally other certificates (see the caPool service annotation for usage) size: 1 # Replica count for the tunnel deployment diff --git a/go.sum b/go.sum index 87abf9a..86ed184 100644 --- a/go.sum +++ b/go.sum @@ -80,7 +80,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= diff --git a/main.go b/main.go index 16a8bdc..a03e1e2 100644 --- a/main.go +++ b/main.go @@ -106,6 +106,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ClusterTunnel") os.Exit(1) } + if err = (&controllers.AccessServiceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AccessService") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {