diff --git a/api/dns/v1/designate_type.go b/api/dns/v1/designate_type.go new file mode 100644 index 00000000..e63ad006 --- /dev/null +++ b/api/dns/v1/designate_type.go @@ -0,0 +1,39 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DesignateSpec defines the desired state of Designate +type DesignateSpec struct { + Credentials string `json:"credentials"` + CloudName string `json:"cloudname"` +} + +// DesignateStatus defines the observed state of Designate +type DesignateStatus struct { +} + +// +kubebuilder:object:root=true + +// Designate is the Schema for the Designates API +type Designate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DesignateSpec `json:"spec,omitempty"` + Status DesignateStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// DesignateList contains a list of Designate +type DesignateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Designate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Designate{}, &DesignateList{}) +} diff --git a/api/dns/v1/groupversion_info.go b/api/dns/v1/groupversion_info.go new file mode 100644 index 00000000..19003430 --- /dev/null +++ b/api/dns/v1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1 contains API Schema definitions for the infrastructure v1 API group +// +kubebuilder:object:generate=true +// +groupName=dns.openstack.org +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "dns.openstack.org", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/dns/v1/zone_type.go b/api/dns/v1/zone_type.go new file mode 100644 index 00000000..98210a2f --- /dev/null +++ b/api/dns/v1/zone_type.go @@ -0,0 +1,44 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ZoneSpec defines the desired state of Zone +type ZoneSpec struct { + Domain string `json:"domain"` + TTL int `json:"ttl"` + Email string `json:"email"` + Description string `json:"description,omitempty"` + Type string `json:"type,omitempty"` +} + +// ZoneStatus defines the observed state of Zone +type ZoneStatus struct { + // +kubebuilder:validation:Default=Pending + ZoneID string `json:"zoneId"` +} + +// +kubebuilder:object:root=true + +// Zone is the Schema for the Zones API +type Zone struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ZoneSpec `json:"spec,omitempty"` + Status ZoneStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ZoneList contains a list of Zone +type ZoneList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Zone `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Zone{}, &ZoneList{}) +} diff --git a/api/dns/v1/zz_generated.deepcopy.go b/api/dns/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000..33904c33 --- /dev/null +++ b/api/dns/v1/zz_generated.deepcopy.go @@ -0,0 +1,203 @@ +// +build !ignore_autogenerated + +/* + + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +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 *Designate) DeepCopyInto(out *Designate) { + *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 Designate. +func (in *Designate) DeepCopy() *Designate { + if in == nil { + return nil + } + out := new(Designate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Designate) 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 *DesignateList) DeepCopyInto(out *DesignateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Designate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesignateList. +func (in *DesignateList) DeepCopy() *DesignateList { + if in == nil { + return nil + } + out := new(DesignateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DesignateList) 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 *DesignateSpec) DeepCopyInto(out *DesignateSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesignateSpec. +func (in *DesignateSpec) DeepCopy() *DesignateSpec { + if in == nil { + return nil + } + out := new(DesignateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DesignateStatus) DeepCopyInto(out *DesignateStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesignateStatus. +func (in *DesignateStatus) DeepCopy() *DesignateStatus { + if in == nil { + return nil + } + out := new(DesignateStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Zone) DeepCopyInto(out *Zone) { + *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 Zone. +func (in *Zone) DeepCopy() *Zone { + if in == nil { + return nil + } + out := new(Zone) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Zone) 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 *ZoneList) DeepCopyInto(out *ZoneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Zone, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneList. +func (in *ZoneList) DeepCopy() *ZoneList { + if in == nil { + return nil + } + out := new(ZoneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ZoneList) 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 *ZoneSpec) DeepCopyInto(out *ZoneSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneSpec. +func (in *ZoneSpec) DeepCopy() *ZoneSpec { + if in == nil { + return nil + } + out := new(ZoneSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ZoneStatus) DeepCopyInto(out *ZoneStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneStatus. +func (in *ZoneStatus) DeepCopy() *ZoneStatus { + if in == nil { + return nil + } + out := new(ZoneStatus) + in.DeepCopyInto(out) + return out +} diff --git a/builders/zone.go b/builders/zone.go new file mode 100644 index 00000000..968670a7 --- /dev/null +++ b/builders/zone.go @@ -0,0 +1,61 @@ +package builders + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/apimachinery/pkg/runtime" + dnsv1 "opendev.org/vexxhost/openstack-operator/api/dns/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// ZoneBuilder defines the interface to build a Zone +type ZoneBuilder struct { + obj *dnsv1.Zone + owner metav1.Object + scheme *runtime.Scheme +} + +// Zone returns a new service builder +func Zone(existing *dnsv1.Zone, owner metav1.Object, scheme *runtime.Scheme) *ZoneBuilder { + existing.Annotations = map[string]string{} + return &ZoneBuilder{ + obj: existing, + owner: owner, + scheme: scheme, + } +} + +// Annotation sets one set annotation +func (z *ZoneBuilder) Annotation(key, value string) *ZoneBuilder { + z.obj.Annotations[key] = value + return z +} + +// Labels specifies labels for the Zone +func (z *ZoneBuilder) Labels(labels map[string]string) *ZoneBuilder { + z.obj.ObjectMeta.Labels = labels + return z +} + +// Domain sets Domain for the Zone +func (z *ZoneBuilder) Domain(domain string) *ZoneBuilder { + z.obj.Spec.Domain = domain + return z +} + +// TTL sets TTL for the Zone +func (z *ZoneBuilder) TTL(ttl int) *ZoneBuilder { + z.obj.Spec.TTL = ttl + return z +} + +// Email sets TTL for the Email +func (z *ZoneBuilder) Email(email string) *ZoneBuilder { + z.obj.Spec.Email = email + return z +} + +// Build returns a complete Zone object +func (z *ZoneBuilder) Build() error { + return controllerutil.SetControllerReference(z.owner, z.obj, z.scheme) +} diff --git a/chart/crds/dns.openstack.org_designates.yaml b/chart/crds/dns.openstack.org_designates.yaml new file mode 100644 index 00000000..4af56512 --- /dev/null +++ b/chart/crds/dns.openstack.org_designates.yaml @@ -0,0 +1,59 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: designates.dns.openstack.org +spec: + group: dns.openstack.org + names: + kind: Designate + listKind: DesignateList + plural: designates + singular: designate + scope: Namespaced + validation: + openAPIV3Schema: + description: Designate is the Schema for the Designates 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: DesignateSpec defines the desired state of Designate + properties: + cloudname: + type: string + credentials: + type: string + required: + - cloudname + - credentials + type: object + status: + description: DesignateStatus defines the observed state of Designate + type: object + type: object + version: v1 + versions: + - name: v1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/chart/crds/dns.openstack.org_zones.yaml b/chart/crds/dns.openstack.org_zones.yaml new file mode 100644 index 00000000..7cbe84e8 --- /dev/null +++ b/chart/crds/dns.openstack.org_zones.yaml @@ -0,0 +1,71 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: zones.dns.openstack.org +spec: + group: dns.openstack.org + names: + kind: Zone + listKind: ZoneList + plural: zones + singular: zone + scope: Namespaced + validation: + openAPIV3Schema: + description: Zone is the Schema for the Zones 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: ZoneSpec defines the desired state of Zone + properties: + description: + type: string + domain: + type: string + email: + type: string + ttl: + type: integer + type: + type: string + required: + - domain + - email + - ttl + type: object + status: + description: ZoneStatus defines the observed state of Zone + properties: + zoneId: + type: string + required: + - zoneId + type: object + type: object + version: v1 + versions: + - name: v1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/chart/templates/clusterrole.yaml b/chart/templates/clusterrole.yaml index 83d0a9a9..ce394462 100644 --- a/chart/templates/clusterrole.yaml +++ b/chart/templates/clusterrole.yaml @@ -43,6 +43,18 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -56,6 +68,46 @@ rules: - patch - update - watch +- apiGroups: + - dns.openstack.org + resources: + - designates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dns.openstack.org + resources: + - designates/status + verbs: + - get + - patch + - update +- apiGroups: + - dns.openstack.org + resources: + - zones + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dns.openstack.org + resources: + - zones/status + verbs: + - get + - patch + - update - apiGroups: - infrastructure.vexxhost.cloud resources: diff --git a/chart/templates/crds.yaml b/chart/templates/crds.yaml index 102c4262..d9956984 100644 --- a/chart/templates/crds.yaml +++ b/chart/templates/crds.yaml @@ -6,4 +6,10 @@ {{- range $path, $bytes := .Files.Glob "crds/monitoring.coreos.com*.yaml" }} {{ $.Files.Get $path }} {{- end }} +{{- end -}} + +{{- if .Values.crd.dns }} + {{- range $path, $bytes := .Files.Glob "crds/dns.openstack.org*.yaml" }} + {{ $.Files.Get $path }} + {{- end }} {{- end -}} \ No newline at end of file diff --git a/chart/values.yaml b/chart/values.yaml index 4f1da7ce..f6a6e7f7 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -1,2 +1,3 @@ crd: monitoring: true + dns: true diff --git a/config/crd/bases/dns.openstack.org_designates.yaml b/config/crd/bases/dns.openstack.org_designates.yaml new file mode 100644 index 00000000..4af56512 --- /dev/null +++ b/config/crd/bases/dns.openstack.org_designates.yaml @@ -0,0 +1,59 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: designates.dns.openstack.org +spec: + group: dns.openstack.org + names: + kind: Designate + listKind: DesignateList + plural: designates + singular: designate + scope: Namespaced + validation: + openAPIV3Schema: + description: Designate is the Schema for the Designates 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: DesignateSpec defines the desired state of Designate + properties: + cloudname: + type: string + credentials: + type: string + required: + - cloudname + - credentials + type: object + status: + description: DesignateStatus defines the observed state of Designate + type: object + type: object + version: v1 + versions: + - name: v1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/dns.openstack.org_zones.yaml b/config/crd/bases/dns.openstack.org_zones.yaml new file mode 100644 index 00000000..7cbe84e8 --- /dev/null +++ b/config/crd/bases/dns.openstack.org_zones.yaml @@ -0,0 +1,71 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: zones.dns.openstack.org +spec: + group: dns.openstack.org + names: + kind: Zone + listKind: ZoneList + plural: zones + singular: zone + scope: Namespaced + validation: + openAPIV3Schema: + description: Zone is the Schema for the Zones 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: ZoneSpec defines the desired state of Zone + properties: + description: + type: string + domain: + type: string + email: + type: string + ttl: + type: integer + type: + type: string + required: + - domain + - email + - ttl + type: object + status: + description: ZoneStatus defines the observed state of Zone + properties: + zoneId: + type: string + required: + - zoneId + type: object + type: object + version: v1 + versions: + - name: v1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5b593421..e7148d41 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,8 @@ resources: - bases/infrastructure.vexxhost.cloud_rabbitmqs.yaml - bases/monitoring.coreos.com_podmonitors.yaml - bases/monitoring.coreos.com_prometheusrules.yaml +- bases/dns.openstack.org_zones.yaml +- bases/dns.openstack.org_designates.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index fcce9139..437dc10b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -43,6 +43,18 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -56,6 +68,46 @@ rules: - patch - update - watch +- apiGroups: + - dns.openstack.org + resources: + - designates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dns.openstack.org + resources: + - designates/status + verbs: + - get + - patch + - update +- apiGroups: + - dns.openstack.org + resources: + - zones + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dns.openstack.org + resources: + - zones/status + verbs: + - get + - patch + - update - apiGroups: - infrastructure.vexxhost.cloud resources: diff --git a/config/samples/dns_v1_designate.yaml b/config/samples/dns_v1_designate.yaml new file mode 100644 index 00000000..bf38174c --- /dev/null +++ b/config/samples/dns_v1_designate.yaml @@ -0,0 +1,9 @@ +apiVersion: dns.openstack.org/v1 +kind: Designate +metadata: + name: sample + annotations: + "dns.openstack.org/is-default-designate": "true" +spec: + credentials: rc-secret-sample + cloudname: devstack diff --git a/config/samples/dns_v1_zone.yaml b/config/samples/dns_v1_zone.yaml new file mode 100644 index 00000000..867347c5 --- /dev/null +++ b/config/samples/dns_v1_zone.yaml @@ -0,0 +1,10 @@ +apiVersion: dns.openstack.org/v1 +kind: Zone +metadata: + name: sample + annotations: + "dns.openstack.org/designate": "sample" +spec: + domain: example2.com. + ttl: 3600 + email: okozachenko@gmail.com \ No newline at end of file diff --git a/controllers/designate_controller.go b/controllers/designate_controller.go new file mode 100644 index 00000000..f9cd4cab --- /dev/null +++ b/controllers/designate_controller.go @@ -0,0 +1,178 @@ +package controllers + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + dnsv1 "opendev.org/vexxhost/openstack-operator/api/dns/v1" + "opendev.org/vexxhost/openstack-operator/builders" + "opendev.org/vexxhost/openstack-operator/utils/baseutils" + "opendev.org/vexxhost/openstack-operator/utils/k8sutils" + "opendev.org/vexxhost/openstack-operator/utils/openstackutils" +) + +// DesignateReconciler reconciles a Designate object +type DesignateReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + DesignateClient *openstackutils.DesignateClientBuilder +} + +const ( + _autoReconcilePeriod = 15 * time.Second + _designatingAnnotation = "dns.openstack.org/designate" + _defaultDesignatingAnnotation = "dns.openstack.org/is-default-designate" +) + +// +kubebuilder:rbac:groups=dns.openstack.org,resources=designates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=dns.openstack.org,resources=designates/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dns.openstack.org,resources=zones,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=dns.openstack.org,resources=zones/status,verbs=get;update;patch + +// Reconcile does the reconcilication of designate instances +func (r *DesignateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + + var ( + credentials corev1.Secret + designate dnsv1.Designate + ) + ctx := context.Background() + log := r.Log.WithValues("Designate", req.NamespacedName) + labels := map[string]string{ + "app.kubernetes.io/name": "designate", + "app.kubernetes.io/managed-by": "openstack-operator", + } + + // 1 Get designate instance + if err := r.Get(ctx, req.NamespacedName, &designate); err != nil { + log.Error(err, "unable to fetch designate "+req.Name+":"+req.Namespace) + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // 2 Get credentials + if err := r.Get(ctx, types.NamespacedName{ + Namespace: req.Namespace, + Name: designate.Spec.Credentials, + }, &credentials); err != nil { + log.Error(err, "unable to fetch rc secret "+designate.Spec.Credentials+":"+req.Namespace) + return ctrl.Result{}, err + } + credential, ok := credentials.Data["clouds.yaml"] + if !ok { + err := fmt.Errorf("rc secret syntax error ") + log.Error(err, designate.Spec.Credentials+":"+designate.Spec.CloudName) + return ctrl.Result{}, err + } + + // 3 Get designate client + if err := openstackutils.DesignateClient(r.DesignateClient, credential, designate.Spec.CloudName); err != nil { + log.WithValues("resource", "designateClient").WithValues("op", "op").Info("ClientCreationFailed" + err.Error()) + return ctrl.Result{}, err + } + + // 4 Create zone CRs + // 4-1 Get zone list from the designate + desinateZoneSpeclist := map[string]dnsv1.ZoneSpec{} + desinateZoneNameList := []string{} + designateZones, err := r.DesignateClient.ListZone() + if err != nil { + log.WithValues("resource", "Zone").WithValues("op", "op").Info("Error: Get zone list in the designate" + err.Error()) + return ctrl.Result{}, err + } + for _, zone := range designateZones { + desinateZoneNameList = append(desinateZoneNameList, zone.Name) + desinateZoneSpeclist[zone.Name] = dnsv1.ZoneSpec{ + Domain: zone.Name, + Email: zone.Email, + TTL: zone.TTL, + } + } + + log.Info("Get Zone list in the Designate") + log.Info("Zone list in the Designate" + fmt.Sprintf("%v", desinateZoneSpeclist)) + + // 4-2 Get zone list in the cluster + clusterZoneObjectMetalist := map[string]metav1.ObjectMeta{} + clusterZoneNameList := []string{} + clusterZones := &dnsv1.ZoneList{} + + if err := r.List(context.Background(), clusterZones); err != nil { + log.WithValues("resource", "Zone").WithValues("op", "op").Info("Error: Get zone list in the cluster" + err.Error()) + return ctrl.Result{}, err + } + for _, zone := range clusterZones.Items { + clusterZoneNameList = append(clusterZoneNameList, zone.Spec.Domain) + clusterZoneObjectMetalist[zone.Spec.Domain] = metav1.ObjectMeta{ + Name: zone.Name, + Namespace: zone.Namespace, + } + } + log.Info("Zone list in the cluster" + fmt.Sprintf("%v", clusterZoneNameList)) + + clusterOnlyNameList, designateOnlyNameList := baseutils.CompareStrSlice(clusterZoneNameList, desinateZoneNameList) + log.Info("Zone list in the only cluster" + fmt.Sprintf("%v", clusterOnlyNameList)) + log.Info("Zone list in the only designate" + fmt.Sprintf("%v", designateOnlyNameList)) + + // 4-3 Create zone list (designateOnlyNameList) in the cluster + log.Info("Create Zone list in the cluster") + for _, zoneName := range designateOnlyNameList { + + Zone := &dnsv1.Zone{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: strings.ReplaceAll(zoneName[:len(zoneName)-1], ".", "-"), + }, + } + op, err := k8sutils.CreateOrUpdate(ctx, r, Zone, func() error { + return builders.Zone(Zone, &designate, r.Scheme). + Labels(labels). + Annotation(_designatingAnnotation, req.Name). + Domain(zoneName). + TTL(desinateZoneSpeclist[zoneName].TTL). + Email(desinateZoneSpeclist[zoneName].Email). + Build() + }) + if err != nil { + return ctrl.Result{}, err + } + log.WithValues("resource", "Zone").WithValues("op", op).Info("Reconciled") + // err = r.Create(context.Background(), Zone) + // if err != nil { + // log.WithValues("resource", "Zone").WithValues("op", "op").Info("ZoneCreationFailed on Cluster -" + zoneName + ":" + err.Error()) + // return ctrl.Result{}, err + // } + } + + // 4-4 Delete zone list (clusterOnlyNameList) in the cluster + log.Info("Delete Zone list in the cluster") + for _, zoneName := range clusterOnlyNameList { + Zone := &dnsv1.Zone{ + ObjectMeta: clusterZoneObjectMetalist[zoneName], + } + err = r.Delete(context.Background(), Zone) + if err != nil { + log.WithValues("resource", "Zone").WithValues("op", "op").Info("ZoneCreationFailed on Cluster -" + zoneName + ":" + err.Error()) + return ctrl.Result{}, err + } + } + return ctrl.Result{Requeue: true, RequeueAfter: _autoReconcilePeriod}, nil +} + +// SetupWithManager initializes the controller with primary manager +func (r *DesignateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&dnsv1.Designate{}). + Owns(&dnsv1.Zone{}). + Complete(r) +} diff --git a/controllers/zone_controller.go b/controllers/zone_controller.go new file mode 100644 index 00000000..a5a37ac9 --- /dev/null +++ b/controllers/zone_controller.go @@ -0,0 +1,128 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + dnsv1 "opendev.org/vexxhost/openstack-operator/api/dns/v1" + "opendev.org/vexxhost/openstack-operator/utils/baseutils" + "opendev.org/vexxhost/openstack-operator/utils/openstackutils" +) + +// ZoneReconciler reconciles a Zone object +type ZoneReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + DesignateClient *openstackutils.DesignateClientBuilder +} + +// +kubebuilder:rbac:groups=dns.openstack.org,resources=zones,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=dns.openstack.org,resources=zones/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile does the reconcilication for create/update/delete Zone instances +func (r *ZoneReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + + var ( + Zone dnsv1.Zone + designateName string + credentials corev1.Secret + designate dnsv1.Designate + ) + ctx := context.Background() + log := r.Log.WithValues("zone", req.NamespacedName) + + // Get Zone + if err := r.Get(ctx, req.NamespacedName, &Zone); err != nil { + log.Error(err, "unable to fetch Zone"+req.Name+":"+req.Namespace) + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Find the corresponding designate Name + if val, ok := Zone.Annotations[_designatingAnnotation]; ok { + designateName = val + } else if val, ok := Zone.Annotations[_defaultDesignatingAnnotation]; ok { + designateName = val + } else { + err := errors.New("no designate annotation") + log.Error(err, "No designate annotation."+req.Name+":"+req.Namespace) + return ctrl.Result{}, err + } + + // Get designate instance + if err := r.Get(ctx, types.NamespacedName{ + Namespace: req.Namespace, + Name: designateName, + }, &designate); err != nil { + log.Error(err, "unable to fetch corresponding designate "+req.Name+":"+req.Namespace) + return ctrl.Result{}, err + } + + // 2 Get credentials + if err := r.Get(ctx, types.NamespacedName{ + Namespace: req.Namespace, + Name: designate.Spec.Credentials, + }, &credentials); err != nil { + log.Error(err, "unable to fetch rc secret "+designate.Spec.Credentials+":"+req.Namespace) + return ctrl.Result{}, err + } + credential, ok := credentials.Data["clouds.yaml"] + if !ok { + err := fmt.Errorf("rc secret syntax error ") + log.Error(err, designate.Spec.Credentials+":"+designate.Spec.CloudName) + return ctrl.Result{}, err + } + + // 3 Get designate client + if err := openstackutils.DesignateClient(r.DesignateClient, credential, designate.Spec.CloudName); err != nil { + log.WithValues("resource", "designateClient").WithValues("op", "op").Info("ClientCreationFailed" + err.Error()) + return ctrl.Result{}, err + } + + // Use Finalizer for the async deletion + zoneFinalizeName := "zone.finalizers.dns.openstack.org" + if Zone.ObjectMeta.DeletionTimestamp.IsZero() { + if !(baseutils.ContainsString(Zone.ObjectMeta.Finalizers, zoneFinalizeName)) { + Zone.ObjectMeta.Finalizers = append(Zone.ObjectMeta.Finalizers, zoneFinalizeName) + if err := r.Update(ctx, &Zone); err != nil { + return ctrl.Result{}, err + } + } + } else { + if baseutils.ContainsString(Zone.ObjectMeta.Finalizers, zoneFinalizeName) { + if err := r.DesignateClient.DeleteZone(Zone.Spec.Domain); err != nil { + return ctrl.Result{}, err + } + + log.Info("Zone deletion using finalizer") + Zone.ObjectMeta.Finalizers = baseutils.RemoveString(Zone.ObjectMeta.Finalizers, zoneFinalizeName) + if err := r.Update(ctx, &Zone); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // Create or update + if err := r.DesignateClient.CreateOrUpdateZone(Zone.Spec.Domain, Zone.Spec.TTL, Zone.Spec.Email); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// SetupWithManager initializes the controller with primary manager +func (r *ZoneReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&dnsv1.Zone{}). + Complete(r) +} diff --git a/go.mod b/go.mod index 8d6e0c1d..5f7d7958 100755 --- a/go.mod +++ b/go.mod @@ -4,9 +4,15 @@ go 1.13 require ( github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf + github.com/ghodss/yaml v1.0.0 github.com/go-logr/logr v0.1.0 github.com/google/go-cmp v0.3.0 + github.com/gophercloud/gophercloud v0.1.0 + github.com/onsi/ginkgo v1.11.0 + github.com/onsi/gomega v1.8.1 + github.com/sirupsen/logrus v1.4.2 github.com/stretchr/testify v1.5.1 + gopkg.in/yaml.v2 v2.2.4 k8s.io/api v0.17.2 k8s.io/apimachinery v0.17.2 k8s.io/client-go v0.17.2 diff --git a/go.sum b/go.sum index 003c6bb4..56ddbd5c 100755 --- a/go.sum +++ b/go.sum @@ -66,6 +66,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -151,6 +152,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -248,6 +250,7 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= diff --git a/main.go b/main.go index 2a516dac..d206574a 100755 --- a/main.go +++ b/main.go @@ -1,6 +1,4 @@ /* - - 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 @@ -26,9 +24,11 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" + dnsv1 "opendev.org/vexxhost/openstack-operator/api/dns/v1" monitoringv1 "opendev.org/vexxhost/openstack-operator/api/monitoring/v1" infrastructurev1alpha1 "opendev.org/vexxhost/openstack-operator/api/v1alpha1" "opendev.org/vexxhost/openstack-operator/controllers" + "opendev.org/vexxhost/openstack-operator/utils/openstackutils" "opendev.org/vexxhost/openstack-operator/version" // +kubebuilder:scaffold:imports ) @@ -40,9 +40,9 @@ var ( func init() { _ = clientgoscheme.AddToScheme(scheme) - _ = infrastructurev1alpha1.AddToScheme(scheme) _ = monitoringv1.AddToScheme(scheme) + _ = dnsv1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } @@ -54,7 +54,6 @@ func main() { "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.Parse() - ctrl.SetLogger(zap.New(zap.UseDevMode(true))) // Create manager @@ -70,8 +69,53 @@ func main() { os.Exit(1) } + // Get Designate client + designateClientBuilder := new(openstackutils.DesignateClientBuilder) + designateClientBuilder.SetAuthFailed() + // Setup controllers with manager - if err = (&controllers.McrouterReconciler{ + setupMcrouterReconciler(mgr) + setupMemcachedReconciler(mgr) + setupZoneReconciler(mgr, designateClientBuilder) + setupDesignateReconciler(mgr, designateClientBuilder) + + // +kubebuilder:scaffold:builder + setupLog.Info("starting manager", "revision", version.Revision) + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +// setupZoneReconciler setups the Zone controller with manager +func setupZoneReconciler(mgr ctrl.Manager, designateClientBuilder *openstackutils.DesignateClientBuilder) { + if err := (&controllers.ZoneReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Zone"), + Scheme: mgr.GetScheme(), + DesignateClient: designateClientBuilder, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Zone") + os.Exit(1) + } +} + +// setupDesignateReconciler setups the Designate controller with manager +func setupDesignateReconciler(mgr ctrl.Manager, designateClientBuilder *openstackutils.DesignateClientBuilder) { + if err := (&controllers.DesignateReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Zone"), + Scheme: mgr.GetScheme(), + DesignateClient: designateClientBuilder, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Designate") + os.Exit(1) + } +} + +// setupMcrouterReconciler setups the Mcrouter controller with manager +func setupMcrouterReconciler(mgr ctrl.Manager) { + if err := (&controllers.McrouterReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Mcrouter"), Scheme: mgr.GetScheme(), @@ -79,8 +123,11 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Mcrouter") os.Exit(1) } +} - if err = (&controllers.MemcachedReconciler{ +// setupMemcachedReconciler setups the Memcached controller with manager +func setupMemcachedReconciler(mgr ctrl.Manager) { + if err := (&controllers.MemcachedReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Memcached"), Scheme: mgr.GetScheme(), @@ -88,20 +135,4 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Memcached") os.Exit(1) } - - if err = (&controllers.RabbitmqReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Rabbitmq"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Rabbitmq") - os.Exit(1) - } - // +kubebuilder:scaffold:builder - - setupLog.Info("starting manager", "revision", version.Revision) - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { - setupLog.Error(err, "problem running manager") - os.Exit(1) - } } diff --git a/utils/baseutils/codec.go b/utils/baseutils/codec.go new file mode 100644 index 00000000..1d392e20 --- /dev/null +++ b/utils/baseutils/codec.go @@ -0,0 +1,15 @@ +package baseutils + +import ( + "encoding/base64" +) + +// Base64DecodeByte2Str returns plain text as string from the encrypted text as byte array +func Base64DecodeByte2Str(enc []byte) string { + encStr := string(enc) + decStr, err := base64.StdEncoding.DecodeString(encStr) + if err != nil { + return "" + } + return string(decStr) +} diff --git a/utils/baseutils/slice.go b/utils/baseutils/slice.go new file mode 100644 index 00000000..7acec9da --- /dev/null +++ b/utils/baseutils/slice.go @@ -0,0 +1,54 @@ +package baseutils + +// CompareStrSlice compares two string slices and return the different elements +// Return values are 2 arrays; aOnlySlice, and bOnlySlice +func CompareStrSlice(aS []string, bS []string) ([]string, []string) { + aOnlyS := []string{} + for _, a := range aS { + i, isExist := Find(bS, a) + if !isExist { + aOnlyS = append(aOnlyS, a) + } else { + RemoveElement(&bS, i) + } + } + return aOnlyS, bS +} + +// Find is a helper function to find the string in a slice of strings. +func Find(slice []string, val string) (int, bool) { + for i, item := range slice { + if item == val { + return i, true + } + } + return -1, false +} + +// RemoveElement is a helper function to remove the ith string from a slice of strings. +func RemoveElement(a *[]string, i int) { + (*a)[i] = (*a)[len(*a)-1] // Copy last element to index i. + (*a)[len(*a)-1] = "" // Erase last element (write zero value). + (*a) = (*a)[:len(*a)-1] // Truncate the length +} + +// ContainsString is a helper function to check string in a slice of strings +func ContainsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +// RemoveString is a helper function to remove string from a slice of strings. +func RemoveString(slice []string, s string) (result []string) { + for _, item := range slice { + if item == s { + continue + } + result = append(result, item) + } + return +} diff --git a/utils/openstackutils/designate.go b/utils/openstackutils/designate.go new file mode 100644 index 00000000..93505c77 --- /dev/null +++ b/utils/openstackutils/designate.go @@ -0,0 +1,297 @@ +package openstackutils + +import ( + "fmt" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" + "opendev.org/vexxhost/openstack-operator/utils/tlsutils" +) + +const ( + _openAPIVersion = "3" +) + +// DesignateClientBuilder is an implementation of the designateClientInterface +type DesignateClientBuilder struct { + ServiceClient *gophercloud.ServiceClient + isAuth bool +} + +// CloudYAML is for parsing the clouds.yaml +type CloudYAML struct { + Clouds map[string]struct { + Auth struct { + Auth_url string `yaml:"auth_url"` + Project_name string `yaml:"project_name"` + Project_id string `yaml:"project_id"` + Username string `yaml:"username"` + Password string `yaml:"password"` + User_domain_name string `yaml:"user_domain_name"` + Project_domain_name string `yaml:"project_domain_name"` + } `yaml:"auth"` + Region_name string `yaml:"region_name"` + Interface string `yaml:"interface"` + } `yaml:"clouds"` +} + +// DesignateClient is a constructor for the DesignateBuilder +func DesignateClient(existing *DesignateClientBuilder, rc []byte, cloudName string) error { + if existing.GetAuthStatus() { + log.Infof("Already authenticated") + return nil + } + if err := setAuthSettings(rc, cloudName); err != nil { + log.Infof("Authentication failed - %s", err.Error()) + return err + } + serviceClient, err := createDesignateServiceClient() + if err != nil { + log.Infof("createDesignateServiceClient failed - %s", err.Error()) + return err + } + + existing.ServiceClient = serviceClient + existing.SetAuthSuccess() + log.Infof("Authentication success!") + return nil +} + +// CreateZone creates a zone +func (c *DesignateClientBuilder) CreateZone(dn string, ttl int, email string) (string, error) { + // zone create + + createOpts := zones.CreateOpts{ + Name: dn, + Email: email, + Type: "PRIMARY", + TTL: ttl, + Description: "This is a zone.", + } + + res := zones.Create(c.ServiceClient, createOpts) + if res.Err != nil { + log.Errorf("Create Zone failed - %s", res.Err.Error()) + c.SetAuthFailed() + return "", res.Err + } + + log.Infof("Gained zone infor successfully") + zoneInfo, err := res.Extract() + if err != nil { + c.SetAuthFailed() + log.Errorf("Extract zone infor failed") + return "", err + } + return zoneInfo.ID, err + +} + +// UpdateZone updates zone TTL and Email. +func (c *DesignateClientBuilder) UpdateZone(zoneID string, TTL int, Email string) error { + updateOpts := zones.UpdateOpts{ + TTL: TTL, + Email: Email, + } + if err := zones.Update(c.ServiceClient, zoneID, updateOpts).Err; err != nil { + log.Errorf("Update zone failed") + c.SetAuthFailed() + return err + } + return nil +} + +// DeleteZone deletes a zone +func (c *DesignateClientBuilder) DeleteZone(Domain string) error { + zoneList, err := c.ListZone() + if err != nil { + return err + } + for _, zone := range zoneList { + if zone.Name == Domain { + return c.deleteZoneByID(zone.ID) + } + } + log.Infof("No such zone exists to delete.") + return nil +} + +// ListZone gets the zone list +func (c *DesignateClientBuilder) ListZone() ([]zones.Zone, error) { + listOpts := zones.ListOpts{} + allPages, err := zones.List(c.ServiceClient, listOpts).AllPages() + if err != nil { + log.Errorf("List zone list failed") + c.SetAuthFailed() + return []zones.Zone{}, err + } + + allZones, err := zones.ExtractZones(allPages) + if err != nil { + log.Errorf("Extract zone infor from the zone list failed") + c.SetAuthFailed() + return []zones.Zone{}, err + } + return allZones, nil +} + +// CreateOrUpdateZone sync the zone list +func (c *DesignateClientBuilder) CreateOrUpdateZone(Domain string, TTL int, Email string) error { + zoneList, err := c.ListZone() + if err != nil { + return err + } + for _, zone := range zoneList { + if Domain == zone.Name { + // Update Zone + log.Infof("Designate: Zone %s already exists", zone.Name) + return c.UpdateZone(zone.ID, TTL, Email) + } + } + + // Create zone + _, err = c.CreateZone(Domain, TTL, Email) + + return err +} + +// deleteZoneByID deletes the zone using zoneID, consuming the designate API directly +func (c *DesignateClientBuilder) deleteZoneByID(zoneID string) error { + if err := zones.Delete(c.ServiceClient, zoneID).Err; err != nil { + c.SetAuthFailed() + return err + } + return nil +} + +// SetAuthSuccess means the current client already authenticated +func (c *DesignateClientBuilder) SetAuthSuccess() { + c.isAuth = true +} + +// SetAuthFailed means the current client needs to authenticate +func (c *DesignateClientBuilder) SetAuthFailed() { + c.isAuth = false +} + +// GetAuthStatus returns the authentication status +func (c *DesignateClientBuilder) GetAuthStatus() bool { + return c.isAuth +} + +// authenticate in OpenStack and obtain Designate service endpoint +func createDesignateServiceClient() (*gophercloud.ServiceClient, error) { + opts, err := getAuthSettings() + if err != nil { + return nil, err + } + log.Infof("Using OpenStack Keystone at %s", opts.IdentityEndpoint) + authProvider, err := openstack.NewClient(opts.IdentityEndpoint) + if err != nil { + return nil, err + } + + tlsConfig, err := tlsutils.CreateTLSConfig("OPENSTACK") + if err != nil { + return nil, err + } + + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: tlsConfig, + } + authProvider.HTTPClient.Transport = transport + + if err = openstack.Authenticate(authProvider, opts); err != nil { + return nil, err + } + + eo := gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + } + + client, err := openstack.NewDNSV2(authProvider, eo) + if err != nil { + return nil, err + } + log.Infof("Found OpenStack Designate service at %s", client.Endpoint) + return client, nil +} + +// returns OpenStack Keystone authentication settings by obtaining values from standard environment variables. +// also fixes incompatibilities between gophercloud implementation and *-stackrc files that can be downloaded +// from OpenStack dashboard in latest versions +func getAuthSettings() (gophercloud.AuthOptions, error) { + remapEnv(map[string]string{ + "OS_TENANT_NAME": "OS_PROJECT_NAME", + "OS_TENANT_ID": "OS_PROJECT_ID", + "OS_DOMAIN_NAME": "OS_USER_DOMAIN_NAME", + "OS_DOMAIN_ID": "OS_USER_DOMAIN_ID", + }) + + opts, err := openstack.AuthOptionsFromEnv() + if err != nil { + return gophercloud.AuthOptions{}, err + } + opts.AllowReauth = true + if !strings.HasSuffix(opts.IdentityEndpoint, "/") { + opts.IdentityEndpoint += "/" + } + if !strings.HasSuffix(opts.IdentityEndpoint, "/v2.0/") && !strings.HasSuffix(opts.IdentityEndpoint, "/v3/") { + opts.IdentityEndpoint += "v2.0/" + } + return opts, nil +} + +// copies environment variables to new names without overwriting existing values +func remapEnv(mapping map[string]string) { + for k, v := range mapping { + currentVal := os.Getenv(k) + newVal := os.Getenv(v) + if currentVal == "" && newVal != "" { + os.Setenv(k, newVal) + } + } +} + +func setAuthSettings(rc []byte, cloudName string) error { + var cloudYaml CloudYAML + parseCloudYAML(rc, &cloudYaml) + credential, ok := cloudYaml.Clouds[cloudName] + if !ok { + return fmt.Errorf("rc secret does not involve the current cloud credential ") + } + os.Setenv("OS_AUTH_URL", credential.Auth.Auth_url) + os.Setenv("OS_PROJECT_ID", credential.Auth.Project_id) + os.Setenv("OS_PROJECT_NAME", credential.Auth.Project_name) + os.Setenv("OS_USER_DOMAIN_NAME", credential.Auth.User_domain_name) + os.Setenv("OS_USERNAME", credential.Auth.Username) + os.Setenv("OS_PASSWORD", credential.Auth.Password) + os.Setenv("OS_REGION_NAME", credential.Region_name) + os.Setenv("OS_INTERFACE", credential.Interface) + os.Setenv("OS_IDENTITY_API_VERSION", _openAPIVersion) + return nil +} + +func parseCloudYAML(y []byte, cloudYaml *CloudYAML) { + err := yaml.Unmarshal([]byte(y), cloudYaml) + if err != nil { + panic(err) + } +} diff --git a/utils/tlsutils/tls.go b/utils/tlsutils/tls.go new file mode 100644 index 00000000..5c5375a1 --- /dev/null +++ b/utils/tlsutils/tls.go @@ -0,0 +1,73 @@ +package tlsutils + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" +) + +const defaultMinVersion = 0 + +// CreateTLSConfig creates tls.Config instance from TLS parameters passed in environment variables with the given prefix +func CreateTLSConfig(prefix string) (*tls.Config, error) { + caFile := os.Getenv(fmt.Sprintf("%s_CA_FILE", prefix)) + certFile := os.Getenv(fmt.Sprintf("%s_CERT_FILE", prefix)) + keyFile := os.Getenv(fmt.Sprintf("%s_KEY_FILE", prefix)) + serverName := os.Getenv(fmt.Sprintf("%s_TLS_SERVER_NAME", prefix)) + isInsecureStr := strings.ToLower(os.Getenv(fmt.Sprintf("%s_TLS_INSECURE", prefix))) + isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1" + tlsConfig, err := NewTLSConfig(certFile, keyFile, caFile, serverName, isInsecure, defaultMinVersion) + if err != nil { + return nil, err + } + return tlsConfig, nil +} + +// NewTLSConfig creates a tls.Config instance from directly-passed parameters, loading the ca, cert, and key from disk +func NewTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool, minVersion uint16) (*tls.Config, error) { + if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" { + return nil, errors.New("either both cert and key or none must be provided") + } + var certificates []tls.Certificate + if certPath != "" { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("could not load TLS cert: %s", err) + } + certificates = append(certificates, cert) + } + roots, err := loadRoots(caPath) + if err != nil { + return nil, err + } + + return &tls.Config{ + MinVersion: minVersion, + Certificates: certificates, + RootCAs: roots, + InsecureSkipVerify: insecure, + ServerName: serverName, + }, nil +} + +// loads CA cert +func loadRoots(caPath string) (*x509.CertPool, error) { + if caPath == "" { + return nil, nil + } + + roots := x509.NewCertPool() + pem, err := ioutil.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", caPath, err) + } + ok := roots.AppendCertsFromPEM(pem) + if !ok { + return nil, fmt.Errorf("could not read root certs: %s", err) + } + return roots, nil +}