9 Commits

Author SHA1 Message Date
mudler
737bae9d77 🌱 Add daemonset
Signed-off-by: mudler <mudler@c3os.io>
2022-12-28 19:40:55 +01:00
mudler
ca06cf47e4 Change default failurePolicy to Fail 2022-12-27 13:59:14 +01:00
Dimitris Karakasilis
2250450b88 Don't mutate original Pod's labels
because we don't want to, plus annotations may not fit in labels
because they have different character limit.

Fix for error about 63 character limit

Signed-off-by: Dimitris Karakasilis <jimmykarily@gmail.com>
2022-12-06 12:41:42 +00:00
Ettore Di Giacinto
b4a78705dd Inject nethost by default in entangled pods 2022-12-05 16:09:07 +01:00
mudler
b25f31d789 Add kubesplit target 2022-11-14 23:58:12 +01:00
mudler
daab27404c Allow to propagate envs 2022-11-14 23:52:21 +01:00
Ettore Di Giacinto
1da47ac24f Allow to set service image and loglevel from CLI 2022-11-11 08:03:59 +00:00
Ettore Di Giacinto
297f557a04 Read both annotations and labels 2022-11-11 08:03:25 +00:00
Ettore Di Giacinto
c3cd5f5654 Update README.md 2022-09-23 23:08:07 +02:00
24 changed files with 917 additions and 121 deletions

View File

@@ -276,3 +276,8 @@ e2e-tests:
KUBE_VERSION=${KUBE_VERSION} $(ROOT_DIR)/script/test.sh
kind-e2e-tests: kind-setup install undeploy-dev deploy-dev e2e-tests
kubesplit: manifests kustomize
rm -rf helm-chart
mkdir helm-chart
$(KUSTOMIZE) build config/default | kubesplit -helm helm-chart

View File

@@ -19,4 +19,13 @@ resources:
webhooks:
defaulting: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: kairos.io
group: entangle
kind: VPN
path: github.com/kairos-io/entangle/api/v1alpha1
version: v1alpha1
version: "3"

View File

@@ -1,81 +1,34 @@
# entangle
// TODO(user): Add simple overview of use/purpose
## Description
// TODO(user): An in-depth paragraph about your project and overview of use
| :exclamation: | This is experimental! |
|-|:-|
## Getting Started
Youll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster.
**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows).
This is the Kairos entangle Kubernetes Native Extension.
### Running on the cluster
1. Install Instances of Custom Resources:
To install, use helm:
```sh
kubectl apply -f config/samples/
```
# Adds the kairos repo to helm
$ helm repo add kairos https://kairos-io.github.io/helm-charts
"kairos" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "kairos" chart repository
Update Complete. ⎈Happy Helming!⎈
2. Build and push your image to the location specified by `IMG`:
# Install the CRD chart
$ helm install kairos-crd kairos/kairos-crds
NAME: kairos-crd
LAST DEPLOYED: Tue Sep 6 20:35:34 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
```sh
make docker-build docker-push IMG=<some-registry>/entangle:tag
# Installs entangle
$ helm install kairos-entangle kairos/entangle
```
3. Deploy the controller to the cluster with the image specified by `IMG`:
```sh
make deploy IMG=<some-registry>/entangle:tag
```
### Uninstall CRDs
To delete the CRDs from the cluster:
```sh
make uninstall
```
### Undeploy controller
UnDeploy the controller to the cluster:
```sh
make undeploy
```
## Contributing
// TODO(user): Add detailed information on how you would like others to contribute to this project
### How it works
This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/)
which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster
### Test It Out
1. Install the CRDs into the cluster:
```sh
make install
```
2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
```sh
make run
```
**NOTE:** You can also run this in one step by running: `make install run`
### Modifying the API definitions
If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
```sh
make manifests
```
**NOTE:** Run `make --help` for more information on all potential `make` targets
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
## License
Copyright 2022.

View File

@@ -36,6 +36,7 @@ type EntanglementSpec struct {
Port string `json:"port,omitempty"`
HostNetwork bool `json:"hostNetwork,omitempty"`
Inbound bool `json:"inbound,omitempty"`
Envs []v1.EnvVar `json:"env,omitempty"`
// +kubebuilder:validation:Optional
ServiceSpec *v1.ServiceSpec `json:"serviceSpec,omitEmpty"`
}

62
api/v1alpha1/vpn_types.go Normal file
View File

@@ -0,0 +1,62 @@
/*
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 (
v1 "k8s.io/api/core/v1"
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.
// VPNSpec defines the desired state of VPN
type VPNSpec struct {
SecretRef *string `json:"secretRef,omitempty"`
Env []v1.EnvVar `json:"env,omitempty"`
}
// VPNStatus defines the observed state of VPN
type VPNStatus 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
// VPN is the Schema for the vpns API
type VPN struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec VPNSpec `json:"spec,omitempty"`
Status VPNStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// VPNList contains a list of VPN
type VPNList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []VPN `json:"items"`
}
func init() {
SchemeBuilder.Register(&VPN{}, &VPNList{})
}

View File

@@ -98,6 +98,13 @@ func (in *EntanglementSpec) DeepCopyInto(out *EntanglementSpec) {
*out = new(string)
**out = **in
}
if in.Envs != nil {
in, out := &in.Envs, &out.Envs
*out = make([]v1.EnvVar, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ServiceSpec != nil {
in, out := &in.ServiceSpec, &out.ServiceSpec
*out = new(v1.ServiceSpec)
@@ -129,3 +136,104 @@ func (in *EntanglementStatus) DeepCopy() *EntanglementStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VPN) DeepCopyInto(out *VPN) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPN.
func (in *VPN) DeepCopy() *VPN {
if in == nil {
return nil
}
out := new(VPN)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *VPN) 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 *VPNList) DeepCopyInto(out *VPNList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]VPN, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPNList.
func (in *VPNList) DeepCopy() *VPNList {
if in == nil {
return nil
}
out := new(VPNList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *VPNList) 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 *VPNSpec) DeepCopyInto(out *VPNSpec) {
*out = *in
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(string)
**out = **in
}
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = make([]v1.EnvVar, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPNSpec.
func (in *VPNSpec) DeepCopy() *VPNSpec {
if in == nil {
return nil
}
out := new(VPNSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VPNStatus) DeepCopyInto(out *VPNStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPNStatus.
func (in *VPNStatus) DeepCopy() *VPNStatus {
if in == nil {
return nil
}
out := new(VPNStatus)
in.DeepCopyInto(out)
return out
}

View File

@@ -35,6 +35,110 @@ spec:
spec:
description: EntanglementSpec defines the desired state of Entanglement
properties:
env:
items:
description: EnvVar represents an environment variable present in
a Container.
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
type: string
value:
description: 'Variable references $(VAR_NAME) are expanded using
the previously defined environment variables in the container
and any service environment variables. If a variable cannot
be resolved, the reference in the input string will be unchanged.
Double $$ are reduced to a single $, which allows for escaping
the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the
string literal "$(VAR_NAME)". Escaped references will never
be expanded, regardless of whether the variable exists or
not. Defaults to "".'
type: string
valueFrom:
description: Source for the environment variable's value. Cannot
be used if value is not empty.
properties:
configMapKeyRef:
description: Selects a key of a ConfigMap.
properties:
key:
description: The key to select.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the ConfigMap or its key
must be defined
type: boolean
required:
- key
type: object
fieldRef:
description: 'Selects a field of the pod: supports metadata.name,
metadata.namespace, `metadata.labels[''<KEY>'']`, `metadata.annotations[''<KEY>'']`,
spec.nodeName, spec.serviceAccountName, status.hostIP,
status.podIP, status.podIPs.'
properties:
apiVersion:
description: Version of the schema the FieldPath is
written in terms of, defaults to "v1".
type: string
fieldPath:
description: Path of the field to select in the specified
API version.
type: string
required:
- fieldPath
type: object
resourceFieldRef:
description: 'Selects a resource of the container: only
resources limits and requests (limits.cpu, limits.memory,
limits.ephemeral-storage, requests.cpu, requests.memory
and requests.ephemeral-storage) are currently supported.'
properties:
containerName:
description: 'Container name: required for volumes,
optional for env vars'
type: string
divisor:
anyOf:
- type: integer
- type: string
description: Specifies the output format of the exposed
resources, defaults to "1"
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
resource:
description: 'Required: resource to select'
type: string
required:
- resource
type: object
secretKeyRef:
description: Selects a key of a secret in the pod's namespace
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the Secret or its key must
be defined
type: boolean
required:
- key
type: object
type: object
required:
- name
type: object
type: array
host:
type: string
hostNetwork:

View File

@@ -0,0 +1,152 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.9.0
creationTimestamp: null
name: vpns.entangle.kairos.io
spec:
group: entangle.kairos.io
names:
kind: VPN
listKind: VPNList
plural: vpns
singular: vpn
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: VPN is the Schema for the vpns 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: VPNSpec defines the desired state of VPN
properties:
env:
items:
description: EnvVar represents an environment variable present in
a Container.
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
type: string
value:
description: 'Variable references $(VAR_NAME) are expanded using
the previously defined environment variables in the container
and any service environment variables. If a variable cannot
be resolved, the reference in the input string will be unchanged.
Double $$ are reduced to a single $, which allows for escaping
the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the
string literal "$(VAR_NAME)". Escaped references will never
be expanded, regardless of whether the variable exists or
not. Defaults to "".'
type: string
valueFrom:
description: Source for the environment variable's value. Cannot
be used if value is not empty.
properties:
configMapKeyRef:
description: Selects a key of a ConfigMap.
properties:
key:
description: The key to select.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the ConfigMap or its key
must be defined
type: boolean
required:
- key
type: object
fieldRef:
description: 'Selects a field of the pod: supports metadata.name,
metadata.namespace, `metadata.labels[''<KEY>'']`, `metadata.annotations[''<KEY>'']`,
spec.nodeName, spec.serviceAccountName, status.hostIP,
status.podIP, status.podIPs.'
properties:
apiVersion:
description: Version of the schema the FieldPath is
written in terms of, defaults to "v1".
type: string
fieldPath:
description: Path of the field to select in the specified
API version.
type: string
required:
- fieldPath
type: object
resourceFieldRef:
description: 'Selects a resource of the container: only
resources limits and requests (limits.cpu, limits.memory,
limits.ephemeral-storage, requests.cpu, requests.memory
and requests.ephemeral-storage) are currently supported.'
properties:
containerName:
description: 'Container name: required for volumes,
optional for env vars'
type: string
divisor:
anyOf:
- type: integer
- type: string
description: Specifies the output format of the exposed
resources, defaults to "1"
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
resource:
description: 'Required: resource to select'
type: string
required:
- resource
type: object
secretKeyRef:
description: Selects a key of a secret in the pod's namespace
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the Secret or its key must
be defined
type: boolean
required:
- key
type: object
type: object
required:
- name
type: object
type: array
secretRef:
type: string
type: object
status:
description: VPNStatus defines the observed state of VPN
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -3,17 +3,20 @@
# It should be run by config/default
resources:
- bases/entangle.kairos.io_entanglements.yaml
- bases/entangle.kairos.io_vpns.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
#- patches/webhook_in_entanglements.yaml
#- patches/webhook_in_vpns.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
# patches here are for enabling the CA injection for each CRD
#- patches/cainjection_in_entanglements.yaml
#- patches/cainjection_in_vpns.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
# the following config is for teaching kustomize how to do kustomization for CRDs.

View File

@@ -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: vpns.entangle.kairos.io

View File

@@ -0,0 +1,16 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: vpns.entangle.kairos.io
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1

View File

@@ -23,6 +23,18 @@ rules:
- get
- list
- watch
- apiGroups:
- apps
resources:
- daemonsets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
@@ -61,3 +73,29 @@ rules:
- get
- patch
- update
- apiGroups:
- entangle.kairos.io
resources:
- vpns
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- entangle.kairos.io
resources:
- vpns/finalizers
verbs:
- update
- apiGroups:
- entangle.kairos.io
resources:
- vpns/status
verbs:
- get
- patch
- update

View File

@@ -0,0 +1,31 @@
# permissions for end users to edit vpns.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: clusterrole
app.kubernetes.io/instance: vpn-editor-role
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: entangle
app.kubernetes.io/part-of: entangle
app.kubernetes.io/managed-by: kustomize
name: vpn-editor-role
rules:
- apiGroups:
- entangle.kairos.io
resources:
- vpns
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- entangle.kairos.io
resources:
- vpns/status
verbs:
- get

View File

@@ -0,0 +1,27 @@
# permissions for end users to view vpns.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: clusterrole
app.kubernetes.io/instance: vpn-viewer-role
app.kubernetes.io/component: rbac
app.kubernetes.io/created-by: entangle
app.kubernetes.io/part-of: entangle
app.kubernetes.io/managed-by: kustomize
name: vpn-viewer-role
rules:
- apiGroups:
- entangle.kairos.io
resources:
- vpns
verbs:
- get
- list
- watch
- apiGroups:
- entangle.kairos.io
resources:
- vpns/status
verbs:
- get

View File

@@ -0,0 +1,12 @@
apiVersion: entangle.kairos.io/v1alpha1
kind: VPN
metadata:
labels:
app.kubernetes.io/name: vpn
app.kubernetes.io/instance: vpn-sample
app.kubernetes.io/part-of: entangle
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/created-by: entangle
name: vpn-sample
spec:
# TODO(user): Add fields here

View File

@@ -1,4 +1,5 @@
## Append samples you want in your CSV to this file as resources ##
resources:
- entangle_v1alpha1_entanglement.yaml
- entangle_v1alpha1_vpn.yaml
#+kubebuilder:scaffold:manifestskustomizesamples

View File

@@ -13,7 +13,7 @@ webhooks:
name: webhook-service
namespace: system
path: /mutate-entangle-kairos-x-io-v1alpha1-entanglement
failurePolicy: Ignore
failurePolicy: Fail
name: mentanglement.kb.io
rules:
- apiGroups:

87
controllers/daemonset.go Normal file
View File

@@ -0,0 +1,87 @@
package controllers
import (
entanglev1alpha1 "github.com/kairos-io/entangle/api/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func genDaemonsetOwner(ent entanglev1alpha1.VPN) []metav1.OwnerReference {
return []metav1.OwnerReference{
*metav1.NewControllerRef(&ent.ObjectMeta, schema.GroupVersionKind{
Group: entanglev1alpha1.GroupVersion.Group,
Version: entanglev1alpha1.GroupVersion.Version,
Kind: "VPN",
}),
}
}
func (r *VPNReconciler) genDaemonset(ent entanglev1alpha1.VPN) (*appsv1.DaemonSet, error) {
objMeta := metav1.ObjectMeta{
Name: ent.Name,
Namespace: ent.Namespace,
OwnerReferences: genDaemonsetOwner(ent),
}
privileged := true
serviceAccount := false
v := ent.Spec.Env
v = append(v, v1.EnvVar{
Name: "EDGEVPNTOKEN",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
Key: "network_token",
LocalObjectReference: v1.LocalObjectReference{
Name: *ent.Spec.SecretRef,
},
},
},
})
expose := v1.Container{
SecurityContext: &v1.SecurityContext{
Privileged: &privileged,
Capabilities: &v1.Capabilities{
Add: []v1.Capability{"NET_ADMIN"},
},
},
ImagePullPolicy: v1.PullAlways,
Name: "vpn",
Image: r.EntangleServiceImage,
Env: v,
Command: []string{"/usr/bin/edgevpn"},
VolumeMounts: []v1.VolumeMount{v1.VolumeMount{Name: "dev-net-tun", MountPath: "/dev/net/tun"}},
}
pod := v1.PodSpec{
Containers: []v1.Container{expose},
AutomountServiceAccountToken: &serviceAccount,
HostNetwork: true,
Volumes: []v1.Volume{v1.Volume{Name: "dev-net-tun", VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/dev/net/tun"}}}},
}
deploymentLabels := getnDaemonsetLabel(ent.Name)
return &appsv1.DaemonSet{
ObjectMeta: objMeta,
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{MatchLabels: deploymentLabels},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deploymentLabels,
},
Spec: pod,
},
},
}, nil
}
func getnDaemonsetLabel(s string) map[string]string {
return map[string]string{
"vpn.kairos.io": s,
}
}

View File

@@ -22,7 +22,7 @@ func genOwner(ent entanglev1alpha1.Entanglement) []metav1.OwnerReference {
}
}
func (r *EntanglementReconciler) genDeployment(ent entanglev1alpha1.Entanglement) (*appsv1.Deployment, error) {
func (r *EntanglementReconciler) genDeployment(ent entanglev1alpha1.Entanglement, logLevel string) (*appsv1.Deployment, error) {
objMeta := metav1.ObjectMeta{
Name: ent.Name,
Namespace: ent.Namespace,
@@ -40,13 +40,8 @@ func (r *EntanglementReconciler) genDeployment(ent entanglev1alpha1.Entanglement
}
}
expose := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "entanglement",
Image: r.EntangleServiceImage,
Env: []v1.EnvVar{
{
v := ent.Spec.Envs
v = append(v, v1.EnvVar{
Name: "EDGEVPNTOKEN",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
@@ -56,8 +51,14 @@ func (r *EntanglementReconciler) genDeployment(ent entanglev1alpha1.Entanglement
},
},
},
},
},
})
expose := v1.Container{
ImagePullPolicy: v1.PullAlways,
SecurityContext: &v1.SecurityContext{Privileged: &privileged},
Name: "entanglement",
Image: r.EntangleServiceImage,
Env: v,
Command: []string{"/usr/bin/edgevpn"},
}
@@ -96,9 +97,9 @@ func (r *EntanglementReconciler) genDeployment(ent entanglev1alpha1.Entanglement
}
if ent.Spec.ServiceRef != nil {
expose.Args = []string{cmd, "--log-level", "debug", ent.Spec.ServiceUUID, fmt.Sprintf("%s:%s", fmt.Sprintf("%s.svc.cluster.local", svc.Name), ent.Spec.Port)}
expose.Args = []string{cmd, "--log-level", logLevel, ent.Spec.ServiceUUID, fmt.Sprintf("%s:%s", fmt.Sprintf("%s.svc.cluster.local", svc.Name), ent.Spec.Port)}
} else {
expose.Args = []string{cmd, "--log-level", "debug", ent.Spec.ServiceUUID, fmt.Sprintf("%s:%s", ent.Spec.Host, ent.Spec.Port)}
expose.Args = []string{cmd, "--log-level", logLevel, ent.Spec.ServiceUUID, fmt.Sprintf("%s:%s", ent.Spec.Host, ent.Spec.Port)}
}
pod := v1.PodSpec{

View File

@@ -38,7 +38,7 @@ type EntanglementReconciler struct {
clientSet *kubernetes.Clientset
client.Client
Scheme *runtime.Scheme
EntangleServiceImage string
EntangleServiceImage, LogLevel string
}
//+kubebuilder:rbac:groups=entangle.kairos.io,resources=entanglements,verbs=get;list;watch;create;update;patch;delete
@@ -70,7 +70,7 @@ func (r *EntanglementReconciler) Reconcile(ctx context.Context, req ctrl.Request
return ctrl.Result{}, err
}
desiredDeployment, err := r.genDeployment(ent)
desiredDeployment, err := r.genDeployment(ent, r.LogLevel)
if err != nil {
return ctrl.Result{}, err
}

View File

@@ -0,0 +1,114 @@
/*
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"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
entanglev1alpha1 "github.com/kairos-io/entangle/api/v1alpha1"
)
// VPNReconciler reconciles a VPN object
type VPNReconciler struct {
client.Client
Scheme *runtime.Scheme
clientSet *kubernetes.Clientset
EntangleServiceImage string
}
//+kubebuilder:rbac:groups=entangle.kairos.io,resources=vpns,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=entangle.kairos.io,resources=vpns/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=entangle.kairos.io,resources=vpns/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=secrets,verbs=create;get;list;watch
// 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 VPN 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 *VPNReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// Creates a deployment targeting a service
// TODO(user): your logic here
var ent entanglev1alpha1.VPN
if err := r.Get(ctx, req.NamespacedName, &ent); err != nil {
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
desiredDaemonset, err := r.genDaemonset(ent)
if err != nil {
return ctrl.Result{}, err
}
daemonset, err := r.clientSet.AppsV1().DaemonSets(req.Namespace).Get(ctx, desiredDaemonset.Name, v1.GetOptions{})
if daemonset == nil || apierrors.IsNotFound(err) {
logger.Info(fmt.Sprintf("Creating Daemonset %v", daemonset))
daemonset, err = r.clientSet.AppsV1().DaemonSets(req.Namespace).Create(ctx, desiredDaemonset, v1.CreateOptions{})
if err != nil {
logger.Error(err, "Failed while creating daemonset")
return ctrl.Result{}, nil
}
return ctrl.Result{Requeue: true}, nil
}
if err != nil {
return ctrl.Result{Requeue: true}, err
}
// // If args or env are missing, update it
// if desiredDaemonset.{
// deployment, err = r.clientSet.AppsV1().Deployments(req.Namespace).Update(ctx, desiredDeployment, v1.UpdateOptions{})
// if err != nil {
// logger.Error(err, "Failed while updating deployment")
// return ctrl.Result{}, nil
// }
// }
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *VPNReconciler) SetupWithManager(mgr ctrl.Manager) error {
clientset, err := kubernetes.NewForConfig(mgr.GetConfig())
if err != nil {
return err
}
r.clientSet = clientset
return ctrl.NewControllerManagedBy(mgr).
For(&entanglev1alpha1.VPN{}).
Complete(r)
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,18 +25,20 @@ type Webhook struct {
clientSet *kubernetes.Clientset
Scheme *runtime.Scheme
SidecarImage string
SidecarImage, LogLevel string
}
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
//+kubebuilder:webhook:verbs=create;update,path=/mutate-entangle-kairos-x-io-v1alpha1-entanglement,mutating=true,failurePolicy=ignore,sideEffects=None,groups=core,resources=pods,versions=v1,name=mentanglement.kb.io,admissionReviewVersions={v1,v1alpha1}
//+kubebuilder:webhook:verbs=create;update,path=/mutate-entangle-kairos-x-io-v1alpha1-entanglement,mutating=true,failurePolicy=fail,sideEffects=None,groups=core,resources=pods,versions=v1,name=mentanglement.kb.io,admissionReviewVersions={v1,v1alpha1}
var (
EntanglementNameLabel = "entanglement.kairos.io/name"
EntanglementServiceLabel = "entanglement.kairos.io/service"
EntanglementDirectionLabel = "entanglement.kairos.io/direction"
EntanglementNetHost = "entanglement.kairos.io/nethost"
EntanglementPortLabel = "entanglement.kairos.io/target_port"
EntanglementHostLabel = "entanglement.kairos.io/host"
EnvPrefix = "entanglement.kairos.io/env."
)
func (w *Webhook) SetupWebhookWithManager(mgr manager.Manager) error {
@@ -55,34 +58,77 @@ func (w *Webhook) Mutate(ctx context.Context, request admission.Request, object
_ = log.FromContext(ctx)
pod := object.(*corev1.Pod)
entanglementName, exists := pod.Labels[EntanglementNameLabel]
// Let user use both label and annotations
info := make(map[string]string)
// Annotations take precedence
for k, v := range pod.Labels {
info[k] = v
}
// Annotations take precedence
for k, v := range pod.Annotations {
info[k] = v
}
entanglementName, exists := info[EntanglementNameLabel]
if !exists {
return admission.Allowed("")
}
entanglementPort, exists := pod.Labels[EntanglementPortLabel]
envs := []corev1.EnvVar{
{
Name: "EDGEVPNTOKEN",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
Key: "network_token",
LocalObjectReference: corev1.LocalObjectReference{
Name: entanglementName,
},
},
},
}}
for k, v := range info {
if strings.HasPrefix(k, EnvPrefix) {
env := strings.ReplaceAll(k, EnvPrefix, "")
envs = append(envs, corev1.EnvVar{Name: env, Value: v})
}
}
entanglementPort, exists := info[EntanglementPortLabel]
if !exists {
return admission.Allowed("")
}
cmd := "service-connect"
entanglementDirection, exists := pod.Labels[EntanglementDirectionLabel]
entanglementDirection, exists := info[EntanglementDirectionLabel]
if exists && entanglementDirection == "entangle" {
cmd = "service-add"
}
host := "127.0.0.1"
entanglementHost, exists := pod.Labels[EntanglementHostLabel]
entanglementHost, exists := info[EntanglementHostLabel]
if exists && entanglementHost != "" {
host = entanglementHost
}
entanglementService, exists := pod.Labels[EntanglementServiceLabel]
entanglementService, exists := info[EntanglementServiceLabel]
if !exists {
return admission.Allowed("")
}
podCopy := pod.DeepCopy()
hostNetwork, exists := info[EntanglementNetHost]
// By default it injects hostnetwork, however if set to false it does enforces it to false
if exists && hostNetwork == "false" {
podCopy.Spec.HostNetwork = false
} else {
podCopy.Spec.HostNetwork = true
}
secret, err := w.clientSet.CoreV1().Secrets(request.Namespace).Get(context.Background(), entanglementName, v1.GetOptions{})
if err != nil || secret == nil {
return admission.Denied("entanglement secret not found: " + entanglementName + err.Error())
@@ -99,20 +145,8 @@ func (w *Webhook) Mutate(ctx context.Context, request admission.Request, object
servingContainer := corev1.Container{
ImagePullPolicy: corev1.PullAlways,
Command: []string{"/usr/bin/edgevpn"},
Args: []string{cmd, entanglementService, fmt.Sprintf("%s:%s", host, entanglementPort)},
Env: []corev1.EnvVar{
{
Name: "EDGEVPNTOKEN",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
Key: "network_token",
LocalObjectReference: corev1.LocalObjectReference{
Name: entanglementName,
},
},
},
},
},
Args: []string{cmd, entanglementService, fmt.Sprintf("%s:%s", host, entanglementPort), "--log-level", w.LogLevel},
Env: envs,
SecurityContext: &corev1.SecurityContext{Privileged: &privileged},
Name: "entanglement",
Image: w.SidecarImage,

19
main.go
View File

@@ -57,8 +57,13 @@ func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var serviceImage string
var logLevel string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.StringVar(&serviceImage, "service-image", defaultImage, "The image used to create services.")
flag.StringVar(&logLevel, "service-log-level", "debug", "The log level of the sidecar container.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
@@ -97,20 +102,30 @@ func main() {
if err = (&controllers.EntanglementReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
EntangleServiceImage: defaultImage,
EntangleServiceImage: serviceImage,
LogLevel: logLevel,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Entanglement")
os.Exit(1)
}
if err = (&webhooks.Webhook{
Client: mgr.GetClient(),
SidecarImage: defaultImage,
SidecarImage: serviceImage,
Scheme: mgr.GetScheme(),
LogLevel: logLevel,
}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Pod")
os.Exit(1)
}
if err = (&controllers.VPNReconciler{
Client: mgr.GetClient(),
EntangleServiceImage: serviceImage,
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "VPN")
os.Exit(1)
}
//+kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

16
tests/fixtures/vpn.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Secret
metadata:
name: mysecret
namespace: entangle-system
type: Opaque
stringData:
network_token: b3RwOgogIGRodDoKICAgIGludGVydmFsOiA5MDAwCiAgICBrZXk6IFVSRDVaMkcySDNKSFVESVpYT1VXQkVPN1VEU0g2TUpGUzJGV01QM1dBRVhaMjZRUTJGUkEKICAgIGxlbmd0aDogMzIKICBjcnlwdG86CiAgICBpbnRlcnZhbDogOTAwMAogICAga2V5OiBKTVJKWVZKN0xFR0lZQktFVFNERzVSRFhESkFKM0dTWVJOUTNUTVRQUkpMUkwzWEZYUVpBCiAgICBsZW5ndGg6IDMyCnJvb206IFJYWlZSNURCN1VWVERGRDc0UzRBTEFKNllHRVFDVlQ0WE5VWExFQTRVM0FDN05ESFFLTVEKcmVuZGV6dm91czogYk9PR21WV0lCV1ptbXBtaW9PcXdhc0dyWExlaXpnTVkKbWRuczogUnN4ZWd6eGZTcFlRRUtqQk1lUEFQelROWUV5ZGRlemEKbWF4X21lc3NhZ2Vfc2l6ZTogMjA5NzE1MjAK
---
apiVersion: entangle.kairos.io/v1alpha1
kind: VPN
metadata:
name: test2
namespace: entangle-system
spec:
secretRef: "mysecret"