diff --git a/config/crds/v1/crds.kubeflare.io_accessapplications.yaml b/config/crds/v1/crds.kubeflare.io_accessapplications.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6abee7f3093876382715fccf1dcb4a0751371515 --- /dev/null +++ b/config/crds/v1/crds.kubeflare.io_accessapplications.yaml @@ -0,0 +1,135 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.8 + creationTimestamp: null + name: accessapplications.crds.kubeflare.io +spec: + group: crds.kubeflare.io + names: + kind: AccessApplication + listKind: AccessApplicationList + plural: accessapplications + singular: accessapplication + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSRecord is the Schema for the accessapplication 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: AccessApplicationSpec defines the desired state of AccessApplication + properties: + accessPolicies: + items: + properties: + descision: + type: string + exclude: + items: + type: string + type: array + include: + items: + type: string + type: array + name: + type: string + precendence: + type: integer + require: + items: + type: string + type: array + required: + - descision + - include + - name + type: object + type: array + allowedIdPs: + items: + type: string + type: array + autoRedirectToIndentiy: + type: boolean + corsHeaders: + properties: + allowAllHeaders: + type: boolean + allowAllMethods: + type: boolean + allowAllOrigins: + type: boolean + allowCredentials: + type: boolean + allowedHeader: + items: + type: string + type: array + allowedMethods: + items: + type: string + type: array + allowedOrigins: + items: + type: string + type: array + maxAge: + type: integer + required: + - allowAllHeaders + - allowAllMethods + - allowAllOrigins + - allowCredentials + - allowedHeader + - allowedMethods + - allowedOrigins + - maxAge + type: object + domain: + type: string + name: + type: string + sessionDuration: + type: string + zone: + type: string + required: + - domain + - name + - zone + type: object + status: + description: AccessApplicationStatus defines the observed state of AccessApplicationS + properties: + applicationID: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crds/v1beta1/crds.kubeflare.io_accessapplications.yaml b/config/crds/v1beta1/crds.kubeflare.io_accessapplications.yaml new file mode 100644 index 0000000000000000000000000000000000000000..babcadbed38f5295a1de29739de53e8a23d455ca --- /dev/null +++ b/config/crds/v1beta1/crds.kubeflare.io_accessapplications.yaml @@ -0,0 +1,136 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.8 + creationTimestamp: null + name: accessapplications.crds.kubeflare.io +spec: + group: crds.kubeflare.io + names: + kind: AccessApplication + listKind: AccessApplicationList + plural: accessapplications + singular: accessapplication + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: DNSRecord is the Schema for the accessapplication 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: AccessApplicationSpec defines the desired state of AccessApplication + properties: + accessPolicies: + items: + properties: + descision: + type: string + exclude: + items: + type: string + type: array + include: + items: + type: string + type: array + name: + type: string + precendence: + type: integer + require: + items: + type: string + type: array + required: + - descision + - include + - name + type: object + type: array + allowedIdPs: + items: + type: string + type: array + autoRedirectToIndentiy: + type: boolean + corsHeaders: + properties: + allowAllHeaders: + type: boolean + allowAllMethods: + type: boolean + allowAllOrigins: + type: boolean + allowCredentials: + type: boolean + allowedHeader: + items: + type: string + type: array + allowedMethods: + items: + type: string + type: array + allowedOrigins: + items: + type: string + type: array + maxAge: + type: integer + required: + - allowAllHeaders + - allowAllMethods + - allowAllOrigins + - allowCredentials + - allowedHeader + - allowedMethods + - allowedOrigins + - maxAge + type: object + domain: + type: string + name: + type: string + sessionDuration: + type: string + zone: + type: string + required: + - domain + - name + - zone + type: object + status: + description: AccessApplicationStatus defines the observed state of AccessApplicationS + properties: + applicationID: + type: string + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/go.mod b/go.mod index 6d61f66357df94e1b9b3a52f13877285de905923..25ebb49635f9341e3fbbc6c49c309b3aa4e95887 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/xo/dburl v0.0.0-20200124232849-e9ec94f52bc3 go.uber.org/zap v1.10.0 gopkg.in/yaml.v2 v2.2.8 + gotest.tools v2.2.0+incompatible k8s.io/api v0.18.0 k8s.io/apiextensions-apiserver v0.18.0 k8s.io/apimachinery v0.18.0 diff --git a/go.sum b/go.sum index ad845d8314c3daf9e17826093c94ebfa811dfeef..46837d607b56bdccc645b544e1c4aa9900cbe959 100644 --- a/go.sum +++ b/go.sum @@ -596,6 +596,7 @@ gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 h1:B0J02caTR6tpSJozBJyiAzT6C gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/apis/crds/v1alpha1/accessapplication_types.go b/pkg/apis/crds/v1alpha1/accessapplication_types.go new file mode 100644 index 0000000000000000000000000000000000000000..cbfacd9399450acc00535239bd66caa175266944 --- /dev/null +++ b/pkg/apis/crds/v1alpha1/accessapplication_types.go @@ -0,0 +1,85 @@ +/* +Copyright 2019 Replicated, Inc. + +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" +) + +type AccessPolicy struct { + Decision string `json:"descision"` + Name string `json:"name"` + Include []string `json:"include"` // TODO + Precedence *int `json:"precendence,omitempty"` + Exclude []string `json:"exclude,omitempty"` // TODO + Require []string `json:"require,omitempty"` // TODO +} + +type CORSHeader struct { + AllowedMethods []string `json:"allowedMethods"` + AllowedOrigins []string `json:"allowedOrigins"` + AllowedHeaders []string `json:"allowedHeader"` + AllowAllMethods bool `json:"allowAllMethods"` + AllowAllOrigins bool `json:"allowAllOrigins"` + AllowAllHeaders bool `json:"allowAllHeaders"` + AllowCredentials bool `json:"allowCredentials"` + MaxAge int `json:"maxAge"` +} + +// AccessApplicationSpec defines the desired state of AccessApplication +type AccessApplicationSpec struct { + Zone string `json:"zone"` + Name string `json:"name"` + Domain string `json:"domain"` + SessionDuration string `json:"sessionDuration,omitempty"` + AllowedIdPs []string `json:"allowedIdPs,omitempty"` + AutoRedirectToIdentity *bool `json:"autoRedirectToIndentiy,omitempty"` + CORSHeaders *CORSHeader `json:"corsHeaders,omitempty"` + AccessPolicies []AccessPolicy `json:"accessPolicies,omitempty"` +} + +// AccessApplicationStatus defines the observed state of AccessApplicationS +type AccessApplicationStatus struct { + ApplicationID string `json:"applicationID,omitempty"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DNSRecord is the Schema for the accessapplication API +// +k8s:openapi-gen=true +// +kubebuilder:subresource:status +type AccessApplication struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AccessApplicationSpec `json:"spec,omitempty"` + Status AccessApplicationStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// AccessApplicationList contains a list of DNSRecord +type AccessApplicationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSRecord `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AccessApplication{}, &AccessApplicationList{}) +} diff --git a/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go index a76f9e20d912fcbcdcc414df82150fd7caaea076..303b6b16441343898355b6dc501fab194139c286 100644 --- a/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go @@ -119,6 +119,182 @@ func (in *APITokenStatus) DeepCopy() *APITokenStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessApplication) DeepCopyInto(out *AccessApplication) { + *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 AccessApplication. +func (in *AccessApplication) DeepCopy() *AccessApplication { + if in == nil { + return nil + } + out := new(AccessApplication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AccessApplication) 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 *AccessApplicationList) DeepCopyInto(out *AccessApplicationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSRecord, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessApplicationList. +func (in *AccessApplicationList) DeepCopy() *AccessApplicationList { + if in == nil { + return nil + } + out := new(AccessApplicationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AccessApplicationList) 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 *AccessApplicationSpec) DeepCopyInto(out *AccessApplicationSpec) { + *out = *in + if in.AllowedIdPs != nil { + in, out := &in.AllowedIdPs, &out.AllowedIdPs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AutoRedirectToIdentity != nil { + in, out := &in.AutoRedirectToIdentity, &out.AutoRedirectToIdentity + *out = new(bool) + **out = **in + } + if in.CORSHeaders != nil { + in, out := &in.CORSHeaders, &out.CORSHeaders + *out = new(CORSHeader) + (*in).DeepCopyInto(*out) + } + if in.AccessPolicies != nil { + in, out := &in.AccessPolicies, &out.AccessPolicies + *out = make([]AccessPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessApplicationSpec. +func (in *AccessApplicationSpec) DeepCopy() *AccessApplicationSpec { + if in == nil { + return nil + } + out := new(AccessApplicationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessApplicationStatus) DeepCopyInto(out *AccessApplicationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessApplicationStatus. +func (in *AccessApplicationStatus) DeepCopy() *AccessApplicationStatus { + if in == nil { + return nil + } + out := new(AccessApplicationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessPolicy) DeepCopyInto(out *AccessPolicy) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Precedence != nil { + in, out := &in.Precedence, &out.Precedence + *out = new(int) + **out = **in + } + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Require != nil { + in, out := &in.Require, &out.Require + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessPolicy. +func (in *AccessPolicy) DeepCopy() *AccessPolicy { + if in == nil { + return nil + } + out := new(AccessPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CORSHeader) DeepCopyInto(out *CORSHeader) { + *out = *in + if in.AllowedMethods != nil { + in, out := &in.AllowedMethods, &out.AllowedMethods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedOrigins != nil { + in, out := &in.AllowedOrigins, &out.AllowedOrigins + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedHeaders != nil { + in, out := &in.AllowedHeaders, &out.AllowedHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CORSHeader. +func (in *CORSHeader) DeepCopy() *CORSHeader { + if in == nil { + return nil + } + out := new(CORSHeader) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecord) DeepCopyInto(out *DNSRecord) { *out = *in diff --git a/pkg/cli/managercli/run.go b/pkg/cli/managercli/run.go index 2df12a220ac313ec451d8bf5b5daff0ab67e4e4a..7ac5a49a1ea0d4855292195e4507a1771eab2a77 100644 --- a/pkg/cli/managercli/run.go +++ b/pkg/cli/managercli/run.go @@ -4,6 +4,8 @@ import ( "os" "github.com/replicatedhq/kubeflare/pkg/apis" + accessapplicationcontroller "github.com/replicatedhq/kubeflare/pkg/controller/accessapplication" + dnsrecordcontroller "github.com/replicatedhq/kubeflare/pkg/controller/dnsrecord" zonecontroller "github.com/replicatedhq/kubeflare/pkg/controller/zone" "github.com/replicatedhq/kubeflare/pkg/logger" "github.com/replicatedhq/kubeflare/pkg/version" @@ -65,6 +67,16 @@ func RunCmd() *cobra.Command { os.Exit(1) } + if err := dnsrecordcontroller.Add(mgr); err != nil { + logger.Error(err) + os.Exit(1) + } + + if err := accessapplicationcontroller.Add(mgr); err != nil { + logger.Error(err) + os.Exit(1) + } + if err := webhook.AddToManager(mgr); err != nil { logger.Error(err) os.Exit(1) diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/accessapplication.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/accessapplication.go new file mode 100644 index 0000000000000000000000000000000000000000..7a87642515e3a32651f93e7ccc943d1635cdea22 --- /dev/null +++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/accessapplication.go @@ -0,0 +1,195 @@ +/* +Copyright 2020 Replicated, Inc. + +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 client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1" + scheme "github.com/replicatedhq/kubeflare/pkg/client/kubeflareclientset/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// AccessApplicationsGetter has a method to return a AccessApplicationInterface. +// A group's client should implement this interface. +type AccessApplicationsGetter interface { + AccessApplications(namespace string) AccessApplicationInterface +} + +// AccessApplicationInterface has methods to work with AccessApplication resources. +type AccessApplicationInterface interface { + Create(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.CreateOptions) (*v1alpha1.AccessApplication, error) + Update(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.UpdateOptions) (*v1alpha1.AccessApplication, error) + UpdateStatus(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.UpdateOptions) (*v1alpha1.AccessApplication, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.AccessApplication, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.AccessApplicationList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.AccessApplication, err error) + AccessApplicationExpansion +} + +// accessApplications implements AccessApplicationInterface +type accessApplications struct { + client rest.Interface + ns string +} + +// newAccessApplications returns a AccessApplications +func newAccessApplications(c *CrdsV1alpha1Client, namespace string) *accessApplications { + return &accessApplications{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the accessApplication, and returns the corresponding accessApplication object, and an error if there is any. +func (c *accessApplications) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.AccessApplication, err error) { + result = &v1alpha1.AccessApplication{} + err = c.client.Get(). + Namespace(c.ns). + Resource("accessapplications"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of AccessApplications that match those selectors. +func (c *accessApplications) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.AccessApplicationList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.AccessApplicationList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("accessapplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested accessApplications. +func (c *accessApplications) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("accessapplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a accessApplication and creates it. Returns the server's representation of the accessApplication, and an error, if there is any. +func (c *accessApplications) Create(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.CreateOptions) (result *v1alpha1.AccessApplication, err error) { + result = &v1alpha1.AccessApplication{} + err = c.client.Post(). + Namespace(c.ns). + Resource("accessapplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(accessApplication). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a accessApplication and updates it. Returns the server's representation of the accessApplication, and an error, if there is any. +func (c *accessApplications) Update(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.UpdateOptions) (result *v1alpha1.AccessApplication, err error) { + result = &v1alpha1.AccessApplication{} + err = c.client.Put(). + Namespace(c.ns). + Resource("accessapplications"). + Name(accessApplication.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(accessApplication). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *accessApplications) UpdateStatus(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.UpdateOptions) (result *v1alpha1.AccessApplication, err error) { + result = &v1alpha1.AccessApplication{} + err = c.client.Put(). + Namespace(c.ns). + Resource("accessapplications"). + Name(accessApplication.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(accessApplication). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the accessApplication and deletes it. Returns an error if one occurs. +func (c *accessApplications) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("accessapplications"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *accessApplications) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("accessapplications"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched accessApplication. +func (c *accessApplications) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.AccessApplication, err error) { + result = &v1alpha1.AccessApplication{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("accessapplications"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go index ddacc9020fb61252d5030b31413136a841c95346..ae15e2e0d26d250c0892bea6fba5b7e00ccc3f89 100644 --- a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go +++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go @@ -27,6 +27,7 @@ import ( type CrdsV1alpha1Interface interface { RESTClient() rest.Interface APITokensGetter + AccessApplicationsGetter DNSRecordsGetter ZonesGetter } @@ -40,6 +41,10 @@ func (c *CrdsV1alpha1Client) APITokens(namespace string) APITokenInterface { return newAPITokens(c, namespace) } +func (c *CrdsV1alpha1Client) AccessApplications(namespace string) AccessApplicationInterface { + return newAccessApplications(c, namespace) +} + func (c *CrdsV1alpha1Client) DNSRecords(namespace string) DNSRecordInterface { return newDNSRecords(c, namespace) } diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_accessapplication.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_accessapplication.go new file mode 100644 index 0000000000000000000000000000000000000000..4b7a4c40481c28be571c835a32f07171a00dc29b --- /dev/null +++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_accessapplication.go @@ -0,0 +1,142 @@ +/* +Copyright 2020 Replicated, Inc. + +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 client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeAccessApplications implements AccessApplicationInterface +type FakeAccessApplications struct { + Fake *FakeCrdsV1alpha1 + ns string +} + +var accessapplicationsResource = schema.GroupVersionResource{Group: "crds.kubeflare.io", Version: "v1alpha1", Resource: "accessapplications"} + +var accessapplicationsKind = schema.GroupVersionKind{Group: "crds.kubeflare.io", Version: "v1alpha1", Kind: "AccessApplication"} + +// Get takes name of the accessApplication, and returns the corresponding accessApplication object, and an error if there is any. +func (c *FakeAccessApplications) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.AccessApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(accessapplicationsResource, c.ns, name), &v1alpha1.AccessApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.AccessApplication), err +} + +// List takes label and field selectors, and returns the list of AccessApplications that match those selectors. +func (c *FakeAccessApplications) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.AccessApplicationList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(accessapplicationsResource, accessapplicationsKind, c.ns, opts), &v1alpha1.AccessApplicationList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.AccessApplicationList{ListMeta: obj.(*v1alpha1.AccessApplicationList).ListMeta} + for _, item := range obj.(*v1alpha1.AccessApplicationList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested accessApplications. +func (c *FakeAccessApplications) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(accessapplicationsResource, c.ns, opts)) + +} + +// Create takes the representation of a accessApplication and creates it. Returns the server's representation of the accessApplication, and an error, if there is any. +func (c *FakeAccessApplications) Create(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.CreateOptions) (result *v1alpha1.AccessApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(accessapplicationsResource, c.ns, accessApplication), &v1alpha1.AccessApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.AccessApplication), err +} + +// Update takes the representation of a accessApplication and updates it. Returns the server's representation of the accessApplication, and an error, if there is any. +func (c *FakeAccessApplications) Update(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.UpdateOptions) (result *v1alpha1.AccessApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(accessapplicationsResource, c.ns, accessApplication), &v1alpha1.AccessApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.AccessApplication), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeAccessApplications) UpdateStatus(ctx context.Context, accessApplication *v1alpha1.AccessApplication, opts v1.UpdateOptions) (*v1alpha1.AccessApplication, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(accessapplicationsResource, "status", c.ns, accessApplication), &v1alpha1.AccessApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.AccessApplication), err +} + +// Delete takes name of the accessApplication and deletes it. Returns an error if one occurs. +func (c *FakeAccessApplications) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(accessapplicationsResource, c.ns, name), &v1alpha1.AccessApplication{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeAccessApplications) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(accessapplicationsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.AccessApplicationList{}) + return err +} + +// Patch applies the patch and returns the patched accessApplication. +func (c *FakeAccessApplications) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.AccessApplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(accessapplicationsResource, c.ns, name, pt, data, subresources...), &v1alpha1.AccessApplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.AccessApplication), err +} diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go index 5360f0839761ad68496867c0b2a3def7d319beae..ea2128c8d5a27f385ee7519be03547763601b3e2 100644 --- a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go +++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go @@ -32,6 +32,10 @@ func (c *FakeCrdsV1alpha1) APITokens(namespace string) v1alpha1.APITokenInterfac return &FakeAPITokens{c, namespace} } +func (c *FakeCrdsV1alpha1) AccessApplications(namespace string) v1alpha1.AccessApplicationInterface { + return &FakeAccessApplications{c, namespace} +} + func (c *FakeCrdsV1alpha1) DNSRecords(namespace string) v1alpha1.DNSRecordInterface { return &FakeDNSRecords{c, namespace} } diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go index 8e703fb86304ac24f845129fbdd4396ec7ad646d..f7ede82b128b75e2d5bc6c9a40cc7ae7eb16565d 100644 --- a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go +++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go @@ -20,6 +20,8 @@ package v1alpha1 type APITokenExpansion interface{} +type AccessApplicationExpansion interface{} + type DNSRecordExpansion interface{} type ZoneExpansion interface{} diff --git a/pkg/controller/accessapplication/access_applications.go b/pkg/controller/accessapplication/access_applications.go new file mode 100644 index 0000000000000000000000000000000000000000..f1c7582f5601eca86628a09f64fc1cbed2a76bae --- /dev/null +++ b/pkg/controller/accessapplication/access_applications.go @@ -0,0 +1,216 @@ +package accessapplication + +import ( + "context" + "sort" + + "github.com/cloudflare/cloudflare-go" + "github.com/pkg/errors" + crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1" + "github.com/replicatedhq/kubeflare/pkg/logger" +) + +func ReconcileAccessApplicationInstance(ctx context.Context, instance crdsv1alpha1.AccessApplication, zone *crdsv1alpha1.Zone, cf *cloudflare.API) (*cloudflare.AccessApplication, error) { + logger.Debug("reconcileAccessApplication for zone") + + if instance.Status.ApplicationID == "" { + accessApplication := cloudflare.AccessApplication{ + Name: instance.Spec.Name, + Domain: instance.Spec.Domain, + } + + if instance.Spec.SessionDuration != "" { + accessApplication.SessionDuration = instance.Spec.SessionDuration + } + if instance.Spec.AllowedIdPs != nil && len(instance.Spec.AllowedIdPs) > 0 { + accessApplication.AllowedIdps = instance.Spec.AllowedIdPs + } + if instance.Spec.AutoRedirectToIdentity != nil { + accessApplication.AutoRedirectToIdentity = *instance.Spec.AutoRedirectToIdentity + } + if instance.Spec.CORSHeaders != nil { + corsHeaders := cloudflare.AccessApplicationCorsHeaders{ + AllowedMethods: instance.Spec.CORSHeaders.AllowedMethods, + AllowedOrigins: instance.Spec.CORSHeaders.AllowedOrigins, + AllowedHeaders: instance.Spec.CORSHeaders.AllowedHeaders, + AllowAllMethods: instance.Spec.CORSHeaders.AllowAllMethods, + AllowAllOrigins: instance.Spec.CORSHeaders.AllowAllOrigins, + AllowAllHeaders: instance.Spec.CORSHeaders.AllowAllHeaders, + AllowCredentials: instance.Spec.CORSHeaders.AllowCredentials, + MaxAge: instance.Spec.CORSHeaders.MaxAge, + } + + accessApplication.CorsHeaders = &corsHeaders + } + + if err := createAccessApplication(cf, &accessApplication); err != nil { + return nil, errors.Wrap(err, "failed to create access application") + } + + return &accessApplication, nil + } + + existingApplication, err := getExistingAccessApplicationFromID(instance, zone, cf) + if err != nil { + return nil, errors.Wrap(err, "failed to get existing application") + } + + if existingApplication == nil { + // this happens when we have a status resource with an id but it was deleted from cloudflare + // TODO we need to recreate the app and update the status resource here + return nil, errors.New("application not found, maybe it was deleted. update the status.applicationID to remove it from this resource and reconcile again") + } + + hasChanges := false + + toUpdate := cloudflare.AccessApplication{ + ID: existingApplication.ID, + Name: instance.Spec.Name, + Domain: instance.Spec.Domain, + } + + if instance.Spec.Name != existingApplication.Name { + hasChanges = true + } + if instance.Spec.Domain != existingApplication.Domain { + hasChanges = true + } + + if instance.Spec.SessionDuration != "" { + if instance.Spec.SessionDuration != existingApplication.SessionDuration { + hasChanges = true + toUpdate.SessionDuration = instance.Spec.SessionDuration + } + } + if instance.Spec.AllowedIdPs != nil { + sort.Strings(instance.Spec.AllowedIdPs) + sort.Strings(existingApplication.AllowedIdps) + + idpsChanged := len(instance.Spec.AllowedIdPs) != len(existingApplication.AllowedIdps) + + if !idpsChanged { + for i, v := range instance.Spec.AllowedIdPs { + if v != existingApplication.AllowedIdps[i] { + idpsChanged = true + } + } + } + + if idpsChanged { + hasChanges = true + toUpdate.AllowedIdps = instance.Spec.AllowedIdPs + } + } + if instance.Spec.AutoRedirectToIdentity != nil { + if existingApplication.AutoRedirectToIdentity != *instance.Spec.AutoRedirectToIdentity { + hasChanges = true + toUpdate.AutoRedirectToIdentity = *instance.Spec.AutoRedirectToIdentity + } + } + if instance.Spec.CORSHeaders != nil { + updatedCORSHeaders, err := diffCORSHeaders(existingApplication.CorsHeaders, instance.Spec.CORSHeaders) + if err != nil { + return nil, errors.Wrap(err, "failed to diff cors headers") + } + if updatedCORSHeaders != nil { + hasChanges = true + toUpdate.CorsHeaders = updatedCORSHeaders + } + } + + if hasChanges { + if err := updateAccessApplication(cf, &toUpdate); err != nil { + return nil, errors.Wrap(err, "failed to update access application") + } + + existingApplication = &toUpdate + } + + if len(instance.Spec.AccessPolicies) > 0 { + // TODO + existingAccessPolicies := []cloudflare.AccessPolicy{} // TODO + + toCreate, toUpdate, toDelete, err := diffAccessPolicies(existingAccessPolicies, instance.Spec.AccessPolicies) + if err != nil { + return nil, errors.Wrap(err, "failed to diff access policies") + } + + for _, ap := range toCreate { + _, err := cf.CreateAccessPolicy(cf.AccountID, existingApplication.ID, ap) + if err != nil { + return nil, errors.Wrap(err, "failed to create access policy") + } + } + for _, ap := range toUpdate { + _, err := cf.UpdateAccessPolicy(cf.AccountID, existingApplication.ID, ap) + if err != nil { + return nil, errors.Wrap(err, "failed to update access policy") + } + } + for _, ap := range toDelete { + err := cf.DeleteAccessPolicy(cf.AccountID, existingApplication.ID, ap.ID) + if err != nil { + return nil, errors.Wrap(err, "failed to delete access policy") + } + } + } + + return existingApplication, nil +} + +// getExistingAccessApplicationFromID will look at the status subresource and if there's an ID +// will get that. if not, it will a +func getExistingAccessApplicationFromID(instance crdsv1alpha1.AccessApplication, zone *crdsv1alpha1.Zone, cf *cloudflare.API) (*cloudflare.AccessApplication, error) { + accessApplication, err := cf.AccessApplication(cf.AccountID, instance.Status.ApplicationID) + if err != nil { + return nil, errors.Wrap(err, "failed to get accessapplication from cf") + } + + return &accessApplication, nil +} + +func findExistingAccessApplication(instance crdsv1alpha1.AccessApplication, zone *crdsv1alpha1.Zone, cf *cloudflare.API) (*cloudflare.AccessApplication, error) { + currentPage := cloudflare.PaginationOptions{ + PerPage: 20, + Page: 0, + } + + for { + accessApplications, resultInfo, err := cf.AccessApplications(cf.AccountID, currentPage) + if err != nil { + return nil, errors.Wrap(err, "failed to get page of accessapplications") + } + + for _, accessApplication := range accessApplications { + if accessApplication.Domain == instance.Spec.Domain && accessApplication.Name == instance.Spec.Name { + return &accessApplication, nil + } + } + + currentPage.Page++ + + if currentPage.Page >= resultInfo.TotalPages { + return nil, nil + } + } +} + +func createAccessApplication(cf *cloudflare.API, application *cloudflare.AccessApplication) error { + createdApplication, err := cf.CreateAccessApplication(cf.AccountID, *application) + if err != nil { + return errors.Wrap(err, "failed to create access application") + } + + application = &createdApplication + return nil +} + +func updateAccessApplication(cf *cloudflare.API, application *cloudflare.AccessApplication) error { + updatedApplication, err := cf.UpdateAccessApplication(cf.AccountID, *application) + if err != nil { + return errors.Wrap(err, "failed to update access application") + } + + application = &updatedApplication + return nil +} diff --git a/pkg/controller/accessapplication/access_policies.go b/pkg/controller/accessapplication/access_policies.go new file mode 100644 index 0000000000000000000000000000000000000000..02883cc8541197cbf40cfd85eb074997e797bd89 --- /dev/null +++ b/pkg/controller/accessapplication/access_policies.go @@ -0,0 +1,117 @@ +package accessapplication + +import ( + "github.com/cloudflare/cloudflare-go" + "github.com/pkg/errors" + crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1" + "github.com/replicatedhq/kubeflare/pkg/controller/shared" +) + +func diffAccessPolicies(existings []cloudflare.AccessPolicy, desireds []crdsv1alpha1.AccessPolicy) ([]cloudflare.AccessPolicy, []cloudflare.AccessPolicy, []cloudflare.AccessPolicy, error) { + toCreate := []cloudflare.AccessPolicy{} + toUpdate := []cloudflare.AccessPolicy{} + toDelete := []cloudflare.AccessPolicy{} + + if len(existings) == 0 && len(desireds) == 0 { + return toCreate, toUpdate, toDelete, nil + } + + for _, existing := range existings { + found := false + for _, desired := range desireds { + // TODO we should store these by ID in the status field + // with this implementation, renaming would delete and recreate + if desired.Name == existing.Name { + found = true + + updated, err := diffAccessPolicy(existing, desired) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "failed to diff policies") + } + + if updated != nil { + toUpdate = append(toUpdate, *updated) + } + + goto Found + } + } + + Found: + if !found { + toDelete = append(toDelete, existing) + } + } + + for _, desired := range desireds { + found := false + for _, existing := range existings { + if existing.Name == desired.Name { + found = true + goto Found2 + } + } + + Found2: + if !found { + create := cloudflare.AccessPolicy{ + Name: desired.Name, + Decision: desired.Decision, + Include: shared.StringArrayToInterfaceArray(desired.Include), + Exclude: shared.StringArrayToInterfaceArray(desired.Exclude), + Require: shared.StringArrayToInterfaceArray(desired.Require), + } + + if desired.Precedence != nil { + create.Precedence = *desired.Precedence + } + + toCreate = append(toCreate, create) + } + } + return toCreate, toUpdate, toDelete, nil +} + +// diffAccessPolicy will diff existing to desired. +// if there are diffs, it will return not-nil in the first response param +func diffAccessPolicy(existing cloudflare.AccessPolicy, desired crdsv1alpha1.AccessPolicy) (*cloudflare.AccessPolicy, error) { + hasChanged := false + + if existing.Name != desired.Name { + hasChanged = true + existing.Name = desired.Name + } + + if existing.Decision != desired.Decision { + hasChanged = true + existing.Decision = desired.Decision + } + + if desired.Precedence != nil { + if existing.Precedence != *desired.Precedence { + hasChanged = true + existing.Precedence = *desired.Precedence + } + } + + if !shared.StringSlicesMatch(shared.InterfaceArrayToStringArray(existing.Include), desired.Include) { + hasChanged = true + existing.Include = shared.StringArrayToInterfaceArray(desired.Include) + } + + if !shared.StringSlicesMatch(shared.InterfaceArrayToStringArray(existing.Exclude), desired.Exclude) { + hasChanged = true + existing.Exclude = shared.StringArrayToInterfaceArray(desired.Exclude) + } + + if !shared.StringSlicesMatch(shared.InterfaceArrayToStringArray(existing.Require), desired.Require) { + hasChanged = true + existing.Require = shared.StringArrayToInterfaceArray(desired.Require) + } + + if !hasChanged { + return nil, nil + } + + return &existing, nil +} diff --git a/pkg/controller/accessapplication/accessapplication_controller.go b/pkg/controller/accessapplication/accessapplication_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..ac660189d780a427dd3c21a58213d3a56de9c110 --- /dev/null +++ b/pkg/controller/accessapplication/accessapplication_controller.go @@ -0,0 +1,142 @@ +/* +Copyright 2019 Replicated, Inc. + +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 accessapplication + +import ( + "context" + "time" + + "github.com/pkg/errors" + crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1" + "github.com/replicatedhq/kubeflare/pkg/controller/shared" + "github.com/replicatedhq/kubeflare/pkg/logger" + "k8s.io/apimachinery/pkg/runtime" + kubeinformers "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Add creates a new AccessApplication Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileAccessApplication{ + Client: mgr.GetClient(), + scheme: mgr.GetScheme(), + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("accessapplication-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to AccessApplication + err = c.Watch(&source.Kind{ + Type: &crdsv1alpha1.AccessApplication{}, + }, &handler.EnqueueRequestForObject{}) + if err != nil { + return errors.Wrap(err, "failed to start watch on accessapplication") + } + + generatedClient := kubernetes.NewForConfigOrDie(mgr.GetConfig()) + generatedInformers := kubeinformers.NewSharedInformerFactory(generatedClient, time.Minute) + err = mgr.Add(manager.RunnableFunc(func(s <-chan struct{}) error { + generatedInformers.Start(s) + <-s + return nil + })) + if err != nil { + return err + } + + return nil +} + +var _ reconcile.Reconciler = &ReconcileAccessApplication{} + +// ReconcileAccessApplication reconciles a AccessApplication object +type ReconcileAccessApplication struct { + client.Client + scheme *runtime.Scheme +} + +// Reconcile reads that state of the cluster for a ReconcileAccessApplication object and makes changes based on the state read +// and what is in the Zone.Spec +// +kubebuilder:rbac:groups=crds.kubeflare.io,resources=accessapplications,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=crds.kubeflare.io,resources=accessapplications/status,verbs=get;update;patch +func (r *ReconcileAccessApplication) Reconcile(request reconcile.Request) (reconcile.Result, error) { + // This reconcile loop will be called for all ReconcileAccessApplication objects + // because of the informer that we have set up + ctx := context.Background() + instance := crdsv1alpha1.AccessApplication{} + err := r.Get(ctx, request.NamespacedName, &instance) + if err != nil { + logger.Error(err) + return reconcile.Result{}, err + } + + zone, err := shared.GetZone(ctx, instance.Namespace, instance.Spec.Zone) + if err != nil { + logger.Error(err) + return reconcile.Result{}, err + } + + cf, err := shared.GetCloudflareAPI(ctx, instance.Namespace, zone.Spec.APIToken) + if err != nil { + logger.Error(err) + return reconcile.Result{}, err + } + + // if the instanace status subresource doesn't contain an application id, update it now + if instance.Status.ApplicationID == "" { + existingApplication, err := findExistingAccessApplication(instance, zone, cf) + if err != nil { + logger.Error(err) + return reconcile.Result{}, nil + } + + if existingApplication != nil { + instance.Status.ApplicationID = existingApplication.ID + err := r.Status().Update(ctx, &instance) + if err != nil { + logger.Error(err) + return reconcile.Result{}, nil + } + } + } + + _, err = ReconcileAccessApplicationInstance(ctx, instance, zone, cf) + if err != nil { + logger.Error(err) + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil +} diff --git a/pkg/controller/accessapplication/cors_headers.go b/pkg/controller/accessapplication/cors_headers.go new file mode 100644 index 0000000000000000000000000000000000000000..2b3eed7bc20f8be34a10098802a92ea9d13ee59c --- /dev/null +++ b/pkg/controller/accessapplication/cors_headers.go @@ -0,0 +1,82 @@ +package accessapplication + +import ( + "github.com/cloudflare/cloudflare-go" + "github.com/pkg/errors" + crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1" + "github.com/replicatedhq/kubeflare/pkg/controller/shared" +) + +// diffCORSHeaders will diff existing to desired. +// if there are diffs, it will return not-nil in the first response param +func diffCORSHeaders(existing *cloudflare.AccessApplicationCorsHeaders, desired *crdsv1alpha1.CORSHeader) (*cloudflare.AccessApplicationCorsHeaders, error) { + if existing == nil && desired == nil { + return nil, nil + } + + if existing == nil { + // copy all of the desired + updated := &cloudflare.AccessApplicationCorsHeaders{ + AllowedMethods: desired.AllowedMethods, + AllowedOrigins: desired.AllowedOrigins, + AllowedHeaders: desired.AllowedHeaders, + AllowAllMethods: desired.AllowAllMethods, + AllowAllOrigins: desired.AllowAllOrigins, + AllowAllHeaders: desired.AllowAllHeaders, + AllowCredentials: desired.AllowCredentials, + MaxAge: desired.MaxAge, + } + + return updated, nil + } + + if desired == nil { + // TODO, delete? + return nil, errors.New("deleting cors headers is not yet implemented") + } + + wasUpdated := false + updated := cloudflare.AccessApplicationCorsHeaders{ + AllowedMethods: desired.AllowedMethods, + AllowedOrigins: desired.AllowedOrigins, + AllowedHeaders: desired.AllowedHeaders, + AllowAllMethods: desired.AllowAllMethods, + AllowAllOrigins: desired.AllowAllOrigins, + AllowAllHeaders: desired.AllowAllHeaders, + AllowCredentials: desired.AllowCredentials, + MaxAge: desired.MaxAge, + } + + if !shared.StringSlicesMatch(existing.AllowedMethods, desired.AllowedMethods) { + wasUpdated = true + } + if !shared.StringSlicesMatch(existing.AllowedOrigins, desired.AllowedOrigins) { + wasUpdated = true + } + + if !shared.StringSlicesMatch(existing.AllowedHeaders, desired.AllowedHeaders) { + wasUpdated = true + } + + if existing.AllowAllMethods != desired.AllowAllMethods { + wasUpdated = true + } + + if existing.AllowAllOrigins != desired.AllowAllOrigins { + wasUpdated = true + } + + if existing.AllowAllHeaders != desired.AllowAllHeaders { + wasUpdated = true + } + + if existing.MaxAge != desired.MaxAge { + wasUpdated = true + } + + if !wasUpdated { + return nil, nil + } + + return &updated, nil +} diff --git a/pkg/controller/accessapplication/cors_headers_test.go b/pkg/controller/accessapplication/cors_headers_test.go new file mode 100644 index 0000000000000000000000000000000000000000..00658a3e0114b2908232123e24f29b69c0e11047 --- /dev/null +++ b/pkg/controller/accessapplication/cors_headers_test.go @@ -0,0 +1,90 @@ +package accessapplication + +import ( + "testing" + + "github.com/cloudflare/cloudflare-go" + crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1" + "github.com/stretchr/testify/require" + "gotest.tools/assert" +) + +func Test_diffCORSHeaders(t *testing.T) { + tests := []struct { + name string + existing *cloudflare.AccessApplicationCorsHeaders + desired *crdsv1alpha1.CORSHeader + expected *cloudflare.AccessApplicationCorsHeaders + }{ + { + name: "both are nil", + existing: nil, + desired: nil, + expected: nil, + }, + { + name: "no existing, has desired", + existing: nil, + desired: &crdsv1alpha1.CORSHeader{ + AllowedMethods: []string{"a"}, + AllowedOrigins: []string{"b", "c"}, + AllowedHeaders: []string{}, + AllowAllMethods: false, + AllowAllOrigins: true, + AllowAllHeaders: false, + AllowCredentials: true, + MaxAge: 9999, + }, + expected: &cloudflare.AccessApplicationCorsHeaders{ + AllowedMethods: []string{"a"}, + AllowedOrigins: []string{"b", "c"}, + AllowedHeaders: []string{}, + AllowAllMethods: false, + AllowAllOrigins: true, + AllowAllHeaders: false, + AllowCredentials: true, + MaxAge: 9999, + }, + }, + { + name: "change 1 field", + existing: &cloudflare.AccessApplicationCorsHeaders{ + AllowedMethods: []string{"a"}, + AllowedOrigins: []string{"b", "c"}, + AllowedHeaders: []string{}, + AllowAllMethods: false, + AllowAllOrigins: true, + AllowAllHeaders: false, + AllowCredentials: true, + MaxAge: 9999, + }, + desired: &crdsv1alpha1.CORSHeader{ + AllowedMethods: []string{"a"}, + AllowedOrigins: []string{"b", "c'"}, + AllowedHeaders: []string{}, + AllowAllMethods: false, + AllowAllOrigins: true, + AllowAllHeaders: false, + AllowCredentials: true, + MaxAge: 9999, + }, + expected: &cloudflare.AccessApplicationCorsHeaders{ + AllowedMethods: []string{"a"}, + AllowedOrigins: []string{"b", "c'"}, + AllowedHeaders: []string{}, + AllowAllMethods: false, + AllowAllOrigins: true, + AllowAllHeaders: false, + AllowCredentials: true, + MaxAge: 9999, + }, + }, + } + + for _, test := range tests { + req := require.New(t) + actual, err := diffCORSHeaders(test.existing, test.desired) + req.NoError(err) + assert.DeepEqual(t, test.expected, actual) + } +} diff --git a/pkg/controller/dnsrecord/dns_records.go b/pkg/controller/dnsrecord/dns_records.go index a1352c90a08423d1d1c3bd706cc1234d70e44c6f..dc12f059950399d175a3e9826a105c3d75e46560 100644 --- a/pkg/controller/dnsrecord/dns_records.go +++ b/pkg/controller/dnsrecord/dns_records.go @@ -9,7 +9,7 @@ import ( "github.com/replicatedhq/kubeflare/pkg/logger" ) -func ReconcileDNSRecords(ctx context.Context, instance crdsv1alpha1.DNSRecord, zone *crdsv1alpha1.Zone, cf *cloudflare.API) error { +func ReconcileDNSRecordInstances(ctx context.Context, instance crdsv1alpha1.DNSRecord, zone *crdsv1alpha1.Zone, cf *cloudflare.API) error { logger.Debug("reconcileDNSRecords for zone") zoneID, err := cf.ZoneIDByName(zone.Name) diff --git a/pkg/controller/dnsrecord/dnsrecord_controller.go b/pkg/controller/dnsrecord/dnsrecord_controller.go index 9e56ececa2156590256efa2c80b515b469240931..673a5978e226d53f85613491631127259efc854c 100644 --- a/pkg/controller/dnsrecord/dnsrecord_controller.go +++ b/pkg/controller/dnsrecord/dnsrecord_controller.go @@ -114,7 +114,7 @@ func (r *ReconcileDNSRecord) Reconcile(request reconcile.Request) (reconcile.Res return reconcile.Result{}, err } - if err := ReconcileDNSRecords(ctx, instance, zone, cf); err != nil { + if err := ReconcileDNSRecordInstances(ctx, instance, zone, cf); err != nil { logger.Error(err) return reconcile.Result{}, err } diff --git a/pkg/controller/shared/strings.go b/pkg/controller/shared/strings.go new file mode 100644 index 0000000000000000000000000000000000000000..b16ea9fcd786c6e6bddcae89db612da2a7587d74 --- /dev/null +++ b/pkg/controller/shared/strings.go @@ -0,0 +1,47 @@ +package shared + +import "sort" + +func StringSlicesMatch(a []string, b []string) bool { + // sort on a copy to preserve original order + aa := make([]string, len(a)) + bb := make([]string, len(b)) + + copy(aa, a) + copy(bb, b) + + sort.Strings(aa) + sort.Strings(bb) + + hasChanged := len(aa) != len(bb) + + if !hasChanged { + for i, v := range aa { + if v != bb[i] { + hasChanged = true + } + } + } + + return !hasChanged +} + +func InterfaceArrayToStringArray(in []interface{}) []string { + out := make([]string, len(in)) + + for k, v := range in { + out[k] = v.(string) + } + + return out +} + +func StringArrayToInterfaceArray(in []string) []interface{} { + out := make([]interface{}, len(in)) + + for k, v := range in { + out[k] = v + } + + return out +}