diff --git a/Makefile b/Makefile index 23a5901f9..a8d1df419 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ deploy: manifests # Generate DeepCopy to implement runtime.Object deepcopy: - ./vendor/k8s.io/code-generator/generate-groups.sh deepcopy kubesphere.io/kubesphere/pkg/client kubesphere.io/kubesphere/pkg/apis "servicemesh:v1alpha2" + ./vendor/k8s.io/code-generator/generate-groups.sh deepcopy,lister,informer,client kubesphere.io/kubesphere/pkg/client kubesphere.io/kubesphere/pkg/apis "servicemesh:v1alpha2" # Generate code generate: diff --git a/cmd/controller-manager/app/controllers.go b/cmd/controller-manager/app/controllers.go index 93203c1fc..a93a841d8 100644 --- a/cmd/controller-manager/app/controllers.go +++ b/cmd/controller-manager/app/controllers.go @@ -56,6 +56,7 @@ func AddControllers(mgr manager.Manager, cfg *rest.Config, stopCh <-chan struct{ drController := destinationrule.NewDestinationRuleController(informerFactory.Apps().V1().Deployments(), istioInformer.Networking().V1alpha3().DestinationRules(), informerFactory.Core().V1().Services(), + servicemeshinformer.Servicemesh().V1alpha2().ServicePolicies(), kubeClient, istioclient) diff --git a/cmd/ks-apiserver/app/server.go b/cmd/ks-apiserver/app/server.go index 33d91a216..926a11c0f 100644 --- a/cmd/ks-apiserver/app/server.go +++ b/cmd/ks-apiserver/app/server.go @@ -127,7 +127,7 @@ func initializeESClientConfig() { err := db.Find(&outputs).Error if err != nil { - log.Printf("get logging config failed. Error: %v", err) + log.Printf("get logging config failed. Error: %v", err) return } diff --git a/config/crds/servicemesh_v1alpha2_servicepolicy.yaml b/config/crds/servicemesh_v1alpha2_servicepolicy.yaml new file mode 100644 index 000000000..5176a8569 --- /dev/null +++ b/config/crds/servicemesh_v1alpha2_servicepolicy.yaml @@ -0,0 +1,822 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + labels: + controller-tools.k8s.io: "1.0" + name: servicepolicies.servicemesh.kubesphere.io +spec: + group: servicemesh.kubesphere.io + names: + kind: ServicePolicy + plural: servicepolicies + scope: Namespaced + validation: + openAPIV3Schema: + 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/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/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + selector: + description: Label selector for destination rules. + type: object + template: + description: Template used to create a destination rule + properties: + spec: + description: Spec indicates the behavior of a destination rule. + properties: + host: + description: 'REQUIRED. The name of a service from the service + registry. Service names are looked up from the platform''s + service registry (e.g., Kubernetes services, Consul services, + etc.) and from the hosts declared by [ServiceEntries](#ServiceEntry). + Rules defined for services that do not exist in the service + registry will be ignored. *Note for Kubernetes users*: When + short names are used (e.g. "reviews" instead of "reviews.default.svc.cluster.local"), + Istio will interpret the short name based on the namespace + of the rule, not the service. A rule in the "default" namespace + containing a host "reviews will be interpreted as "reviews.default.svc.cluster.local", + irrespective of the actual namespace associated with the reviews + service. _To avoid potential misconfigurations, it is recommended + to always use fully qualified domain names over short names._ Note + that the host field applies to both HTTP and TCP services.' + type: string + subsets: + description: One or more named sets that represent individual + versions of a service. Traffic policies can be overridden + at subset level. + items: + properties: + labels: + description: REQUIRED. Labels apply a filter over the + endpoints of a service in the service registry. See + route rules for examples of usage. + type: object + name: + description: REQUIRED. Name of the subset. The service + name and the subset name can be used for traffic splitting + in a route rule. + type: string + trafficPolicy: + description: Traffic policies that apply to this subset. + Subsets inherit the traffic policies specified at the + DestinationRule level. Settings specified at the subset + level will override the corresponding settings specified + at the DestinationRule level. + properties: + connectionPool: + description: Settings controlling the volume of connections + to an upstream service + properties: + http: + description: HTTP connection pool settings. + properties: + maxRequestsPerConnection: + description: Maximum number of requests per + connection to a backend. Setting this parameter + to 1 disables keep alive. + format: int32 + type: integer + maxRetries: + description: Maximum number of retries that + can be outstanding to all hosts in a cluster + at a given time. Defaults to 3. + format: int32 + type: integer + type: object + tcp: + description: Settings common to both HTTP and + TCP upstream connections. + properties: + connectTimeout: + description: TCP connection timeout. + type: string + maxConnections: + description: Maximum number of HTTP1 /TCP + connections to a destination host. + format: int32 + type: integer + type: object + type: object + loadBalancer: + description: Settings controlling the load balancer + algorithms. + properties: + consistentHash: + properties: + httpCookie: + description: Hash based on HTTP cookie. + properties: + name: + description: REQUIRED. Name of the cookie. + type: string + path: + description: Path to set for the cookie. + type: string + ttl: + description: REQUIRED. Lifetime of the + cookie. + type: string + required: + - name + - ttl + type: object + httpHeaderName: + description: 'It is required to specify exactly + one of the fields as hash key: HttpHeaderName, + HttpCookie, or UseSourceIP. Hash based on + a specific HTTP header.' + type: string + minimumRingSize: + description: The minimum number of virtual + nodes to use for the hash ring. Defaults + to 1024. Larger ring sizes result in more + granular load distributions. If the number + of hosts in the load balancing pool is larger + than the ring size, each host will be assigned + a single virtual node. + format: int64 + type: integer + useSourceIp: + description: Hash based on the source IP address. + type: boolean + type: object + simple: + description: 'It is required to specify exactly + one of the fields: Simple or ConsistentHash' + type: string + type: object + outlierDetection: + description: Settings controlling eviction of unhealthy + hosts from the load balancing pool + properties: + baseEjectionTime: + description: 'Minimum ejection duration. A host + will remain ejected for a period equal to the + product of minimum ejection duration and the + number of times the host has been ejected. This + technique allows the system to automatically + increase the ejection period for unhealthy upstream + servers. format: 1h/1m/1s/1ms. MUST BE >=1ms. + Default is 30s.' + type: string + consecutiveErrors: + description: Number of errors before a host is + ejected from the connection pool. Defaults to + 5. When the upstream host is accessed over HTTP, + a 5xx return code qualifies as an error. When + the upstream host is accessed over an opaque + TCP connection, connect timeouts and connection + error/failure events qualify as an error. + format: int32 + type: integer + interval: + description: 'Time interval between ejection sweep + analysis. format: 1h/1m/1s/1ms. MUST BE >=1ms. + Default is 10s.' + type: string + maxEjectionPercent: + description: Maximum % of hosts in the load balancing + pool for the upstream service that can be ejected. + Defaults to 10%. + format: int32 + type: integer + type: object + portLevelSettings: + description: Traffic policies specific to individual + ports. Note that port level settings will override + the destination-level settings. Traffic settings + specified at the destination-level will not be inherited + when overridden by port-level settings, i.e. default + values will be applied to fields omitted in port-level + traffic policies. + items: + properties: + connectionPool: + description: Settings controlling the volume + of connections to an upstream service + properties: + http: + description: HTTP connection pool settings. + properties: + maxRequestsPerConnection: + description: Maximum number of requests + per connection to a backend. Setting + this parameter to 1 disables keep + alive. + format: int32 + type: integer + maxRetries: + description: Maximum number of retries + that can be outstanding to all hosts + in a cluster at a given time. Defaults + to 3. + format: int32 + type: integer + type: object + tcp: + description: Settings common to both HTTP + and TCP upstream connections. + properties: + connectTimeout: + description: TCP connection timeout. + type: string + maxConnections: + description: Maximum number of HTTP1 + /TCP connections to a destination + host. + format: int32 + type: integer + type: object + type: object + loadBalancer: + description: Settings controlling the load balancer + algorithms. + properties: + consistentHash: + properties: + httpCookie: + description: Hash based on HTTP cookie. + properties: + name: + description: REQUIRED. Name of the + cookie. + type: string + path: + description: Path to set for the + cookie. + type: string + ttl: + description: REQUIRED. Lifetime + of the cookie. + type: string + required: + - name + - ttl + type: object + httpHeaderName: + description: 'It is required to specify + exactly one of the fields as hash + key: HttpHeaderName, HttpCookie, or + UseSourceIP. Hash based on a specific + HTTP header.' + type: string + minimumRingSize: + description: The minimum number of virtual + nodes to use for the hash ring. Defaults + to 1024. Larger ring sizes result + in more granular load distributions. + If the number of hosts in the load + balancing pool is larger than the + ring size, each host will be assigned + a single virtual node. + format: int64 + type: integer + useSourceIp: + description: Hash based on the source + IP address. + type: boolean + type: object + simple: + description: 'It is required to specify + exactly one of the fields: Simple or ConsistentHash' + type: string + type: object + outlierDetection: + description: Settings controlling eviction of + unhealthy hosts from the load balancing pool + properties: + baseEjectionTime: + description: 'Minimum ejection duration. + A host will remain ejected for a period + equal to the product of minimum ejection + duration and the number of times the host + has been ejected. This technique allows + the system to automatically increase the + ejection period for unhealthy upstream + servers. format: 1h/1m/1s/1ms. MUST BE + >=1ms. Default is 30s.' + type: string + consecutiveErrors: + description: Number of errors before a host + is ejected from the connection pool. Defaults + to 5. When the upstream host is accessed + over HTTP, a 5xx return code qualifies + as an error. When the upstream host is + accessed over an opaque TCP connection, + connect timeouts and connection error/failure + events qualify as an error. + format: int32 + type: integer + interval: + description: 'Time interval between ejection + sweep analysis. format: 1h/1m/1s/1ms. + MUST BE >=1ms. Default is 10s.' + type: string + maxEjectionPercent: + description: Maximum % of hosts in the load + balancing pool for the upstream service + that can be ejected. Defaults to 10%. + format: int32 + type: integer + type: object + port: + description: Specifies the port name or number + of a port on the destination service on which + this policy is being applied. Names must + comply with DNS label syntax (rfc1035) and + therefore cannot collide with numbers. If + there are multiple ports on a service with + the same protocol the names should be of the + form -. + properties: + name: + description: Valid port name + type: string + number: + description: Valid port number + format: int32 + type: integer + type: object + tls: + description: TLS related settings for connections + to the upstream service. + properties: + caCertificates: + description: 'OPTIONAL: The path to the + file containing certificate authority + certificates to use in verifying a presented + server certificate. If omitted, the proxy + will not verify the server''s certificate. + Should be empty if mode is `ISTIO_MUTUAL`.' + type: string + clientCertificate: + description: REQUIRED if mode is `MUTUAL`. + The path to the file holding the client-side + TLS certificate to use. Should be empty + if mode is `ISTIO_MUTUAL`. + type: string + mode: + description: 'REQUIRED: Indicates whether + connections to this port should be secured + using TLS. The value of this field determines + how TLS is enforced.' + type: string + privateKey: + description: REQUIRED if mode is `MUTUAL`. + The path to the file holding the client's + private key. Should be empty if mode is + `ISTIO_MUTUAL`. + type: string + sni: + description: SNI string to present to the + server during TLS handshake. Should be + empty if mode is `ISTIO_MUTUAL`. + type: string + subjectAltNames: + description: A list of alternate names to + verify the subject identity in the certificate. + If specified, the proxy will verify that + the server certificate's subject alt name + matches one of the specified values. Should + be empty if mode is `ISTIO_MUTUAL`. + items: + type: string + type: array + required: + - mode + type: object + required: + - port + type: object + type: array + tls: + description: TLS related settings for connections + to the upstream service. + properties: + caCertificates: + description: 'OPTIONAL: The path to the file containing + certificate authority certificates to use in + verifying a presented server certificate. If + omitted, the proxy will not verify the server''s + certificate. Should be empty if mode is `ISTIO_MUTUAL`.' + type: string + clientCertificate: + description: REQUIRED if mode is `MUTUAL`. The + path to the file holding the client-side TLS + certificate to use. Should be empty if mode + is `ISTIO_MUTUAL`. + type: string + mode: + description: 'REQUIRED: Indicates whether connections + to this port should be secured using TLS. The + value of this field determines how TLS is enforced.' + type: string + privateKey: + description: REQUIRED if mode is `MUTUAL`. The + path to the file holding the client's private + key. Should be empty if mode is `ISTIO_MUTUAL`. + type: string + sni: + description: SNI string to present to the server + during TLS handshake. Should be empty if mode + is `ISTIO_MUTUAL`. + type: string + subjectAltNames: + description: A list of alternate names to verify + the subject identity in the certificate. If + specified, the proxy will verify that the server + certificate's subject alt name matches one of + the specified values. Should be empty if mode + is `ISTIO_MUTUAL`. + items: + type: string + type: array + required: + - mode + type: object + type: object + required: + - name + - labels + type: object + type: array + trafficPolicy: + description: Traffic policies to apply (load balancing policy, + connection pool sizes, outlier detection). + properties: + connectionPool: + description: Settings controlling the volume of connections + to an upstream service + properties: + http: + description: HTTP connection pool settings. + properties: + maxRequestsPerConnection: + description: Maximum number of requests per connection + to a backend. Setting this parameter to 1 disables + keep alive. + format: int32 + type: integer + maxRetries: + description: Maximum number of retries that can + be outstanding to all hosts in a cluster at a + given time. Defaults to 3. + format: int32 + type: integer + type: object + tcp: + description: Settings common to both HTTP and TCP upstream + connections. + properties: + connectTimeout: + description: TCP connection timeout. + type: string + maxConnections: + description: Maximum number of HTTP1 /TCP connections + to a destination host. + format: int32 + type: integer + type: object + type: object + loadBalancer: + description: Settings controlling the load balancer algorithms. + properties: + consistentHash: + properties: + httpCookie: + description: Hash based on HTTP cookie. + properties: + name: + description: REQUIRED. Name of the cookie. + type: string + path: + description: Path to set for the cookie. + type: string + ttl: + description: REQUIRED. Lifetime of the cookie. + type: string + required: + - name + - ttl + type: object + httpHeaderName: + description: 'It is required to specify exactly + one of the fields as hash key: HttpHeaderName, + HttpCookie, or UseSourceIP. Hash based on a specific + HTTP header.' + type: string + minimumRingSize: + description: The minimum number of virtual nodes + to use for the hash ring. Defaults to 1024. Larger + ring sizes result in more granular load distributions. + If the number of hosts in the load balancing pool + is larger than the ring size, each host will be + assigned a single virtual node. + format: int64 + type: integer + useSourceIp: + description: Hash based on the source IP address. + type: boolean + type: object + simple: + description: 'It is required to specify exactly one + of the fields: Simple or ConsistentHash' + type: string + type: object + outlierDetection: + description: Settings controlling eviction of unhealthy + hosts from the load balancing pool + properties: + baseEjectionTime: + description: 'Minimum ejection duration. A host will + remain ejected for a period equal to the product of + minimum ejection duration and the number of times + the host has been ejected. This technique allows the + system to automatically increase the ejection period + for unhealthy upstream servers. format: 1h/1m/1s/1ms. + MUST BE >=1ms. Default is 30s.' + type: string + consecutiveErrors: + description: Number of errors before a host is ejected + from the connection pool. Defaults to 5. When the + upstream host is accessed over HTTP, a 5xx return + code qualifies as an error. When the upstream host + is accessed over an opaque TCP connection, connect + timeouts and connection error/failure events qualify + as an error. + format: int32 + type: integer + interval: + description: 'Time interval between ejection sweep analysis. + format: 1h/1m/1s/1ms. MUST BE >=1ms. Default is 10s.' + type: string + maxEjectionPercent: + description: Maximum % of hosts in the load balancing + pool for the upstream service that can be ejected. + Defaults to 10%. + format: int32 + type: integer + type: object + portLevelSettings: + description: Traffic policies specific to individual ports. + Note that port level settings will override the destination-level + settings. Traffic settings specified at the destination-level + will not be inherited when overridden by port-level settings, + i.e. default values will be applied to fields omitted + in port-level traffic policies. + items: + properties: + connectionPool: + description: Settings controlling the volume of connections + to an upstream service + properties: + http: + description: HTTP connection pool settings. + properties: + maxRequestsPerConnection: + description: Maximum number of requests per + connection to a backend. Setting this parameter + to 1 disables keep alive. + format: int32 + type: integer + maxRetries: + description: Maximum number of retries that + can be outstanding to all hosts in a cluster + at a given time. Defaults to 3. + format: int32 + type: integer + type: object + tcp: + description: Settings common to both HTTP and + TCP upstream connections. + properties: + connectTimeout: + description: TCP connection timeout. + type: string + maxConnections: + description: Maximum number of HTTP1 /TCP + connections to a destination host. + format: int32 + type: integer + type: object + type: object + loadBalancer: + description: Settings controlling the load balancer + algorithms. + properties: + consistentHash: + properties: + httpCookie: + description: Hash based on HTTP cookie. + properties: + name: + description: REQUIRED. Name of the cookie. + type: string + path: + description: Path to set for the cookie. + type: string + ttl: + description: REQUIRED. Lifetime of the + cookie. + type: string + required: + - name + - ttl + type: object + httpHeaderName: + description: 'It is required to specify exactly + one of the fields as hash key: HttpHeaderName, + HttpCookie, or UseSourceIP. Hash based on + a specific HTTP header.' + type: string + minimumRingSize: + description: The minimum number of virtual + nodes to use for the hash ring. Defaults + to 1024. Larger ring sizes result in more + granular load distributions. If the number + of hosts in the load balancing pool is larger + than the ring size, each host will be assigned + a single virtual node. + format: int64 + type: integer + useSourceIp: + description: Hash based on the source IP address. + type: boolean + type: object + simple: + description: 'It is required to specify exactly + one of the fields: Simple or ConsistentHash' + type: string + type: object + outlierDetection: + description: Settings controlling eviction of unhealthy + hosts from the load balancing pool + properties: + baseEjectionTime: + description: 'Minimum ejection duration. A host + will remain ejected for a period equal to the + product of minimum ejection duration and the + number of times the host has been ejected. This + technique allows the system to automatically + increase the ejection period for unhealthy upstream + servers. format: 1h/1m/1s/1ms. MUST BE >=1ms. + Default is 30s.' + type: string + consecutiveErrors: + description: Number of errors before a host is + ejected from the connection pool. Defaults to + 5. When the upstream host is accessed over HTTP, + a 5xx return code qualifies as an error. When + the upstream host is accessed over an opaque + TCP connection, connect timeouts and connection + error/failure events qualify as an error. + format: int32 + type: integer + interval: + description: 'Time interval between ejection sweep + analysis. format: 1h/1m/1s/1ms. MUST BE >=1ms. + Default is 10s.' + type: string + maxEjectionPercent: + description: Maximum % of hosts in the load balancing + pool for the upstream service that can be ejected. + Defaults to 10%. + format: int32 + type: integer + type: object + port: + description: Specifies the port name or number of + a port on the destination service on which this + policy is being applied. Names must comply with + DNS label syntax (rfc1035) and therefore cannot + collide with numbers. If there are multiple ports + on a service with the same protocol the names should + be of the form -. + properties: + name: + description: Valid port name + type: string + number: + description: Valid port number + format: int32 + type: integer + type: object + tls: + description: TLS related settings for connections + to the upstream service. + properties: + caCertificates: + description: 'OPTIONAL: The path to the file containing + certificate authority certificates to use in + verifying a presented server certificate. If + omitted, the proxy will not verify the server''s + certificate. Should be empty if mode is `ISTIO_MUTUAL`.' + type: string + clientCertificate: + description: REQUIRED if mode is `MUTUAL`. The + path to the file holding the client-side TLS + certificate to use. Should be empty if mode + is `ISTIO_MUTUAL`. + type: string + mode: + description: 'REQUIRED: Indicates whether connections + to this port should be secured using TLS. The + value of this field determines how TLS is enforced.' + type: string + privateKey: + description: REQUIRED if mode is `MUTUAL`. The + path to the file holding the client's private + key. Should be empty if mode is `ISTIO_MUTUAL`. + type: string + sni: + description: SNI string to present to the server + during TLS handshake. Should be empty if mode + is `ISTIO_MUTUAL`. + type: string + subjectAltNames: + description: A list of alternate names to verify + the subject identity in the certificate. If + specified, the proxy will verify that the server + certificate's subject alt name matches one of + the specified values. Should be empty if mode + is `ISTIO_MUTUAL`. + items: + type: string + type: array + required: + - mode + type: object + required: + - port + type: object + type: array + tls: + description: TLS related settings for connections to the + upstream service. + properties: + caCertificates: + description: 'OPTIONAL: The path to the file containing + certificate authority certificates to use in verifying + a presented server certificate. If omitted, the proxy + will not verify the server''s certificate. Should + be empty if mode is `ISTIO_MUTUAL`.' + type: string + clientCertificate: + description: REQUIRED if mode is `MUTUAL`. The path + to the file holding the client-side TLS certificate + to use. Should be empty if mode is `ISTIO_MUTUAL`. + type: string + mode: + description: 'REQUIRED: Indicates whether connections + to this port should be secured using TLS. The value + of this field determines how TLS is enforced.' + type: string + privateKey: + description: REQUIRED if mode is `MUTUAL`. The path + to the file holding the client's private key. Should + be empty if mode is `ISTIO_MUTUAL`. + type: string + sni: + description: SNI string to present to the server during + TLS handshake. Should be empty if mode is `ISTIO_MUTUAL`. + type: string + subjectAltNames: + description: A list of alternate names to verify the + subject identity in the certificate. If specified, + the proxy will verify that the server certificate's + subject alt name matches one of the specified values. + Should be empty if mode is `ISTIO_MUTUAL`. + items: + type: string + type: array + required: + - mode + type: object + type: object + required: + - host + type: object + type: object + type: object + status: + type: object + version: v1alpha2 +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/samples/servicemesh_v1alpha2_servicepolicy.yaml b/config/samples/servicemesh_v1alpha2_servicepolicy.yaml new file mode 100644 index 000000000..5cd6267f8 --- /dev/null +++ b/config/samples/servicemesh_v1alpha2_servicepolicy.yaml @@ -0,0 +1,9 @@ +apiVersion: servicemesh.kubesphere.io/v1alpha2 +kind: ServicePolicy +metadata: + labels: + controller-tools.k8s.io: "1.0" + name: servicepolicy-sample +spec: + # Add fields here + foo: bar diff --git a/pkg/apis/servicemesh/v1alpha2/servicepolicy_types.go b/pkg/apis/servicemesh/v1alpha2/servicepolicy_types.go new file mode 100644 index 000000000..c49197787 --- /dev/null +++ b/pkg/apis/servicemesh/v1alpha2/servicepolicy_types.go @@ -0,0 +1,127 @@ +/* +Copyright 2019 The KubeSphere authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "github.com/knative/pkg/apis/istio/v1alpha3" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServicePolicySpec defines the desired state of ServicePolicy +type ServicePolicySpec struct { + + // Label selector for destination rules. + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty"` + + // Template used to create a destination rule + // +optional + Template DestinationRuleSpecTemplate `json:"template,omitempty"` +} + +type DestinationRuleSpecTemplate struct { + + // Metadata of the virtual services created from this template + // +optional + metav1.ObjectMeta + + // Spec indicates the behavior of a destination rule. + // +optional + Spec v1alpha3.DestinationRuleSpec `json:"spec,omitempty"` +} + +type ServicePolicyConditionType string + +// These are valid conditions of a strategy. +const ( + // StrategyComplete means the strategy has been delivered to istio. + ServicePolicyComplete ServicePolicyConditionType = "Complete" + + // StrategyFailed means the strategy has failed its delivery to istio. + ServicePolicyFailed ServicePolicyConditionType = "Failed" +) + +// StrategyCondition describes current state of a strategy. +type ServicePolicyCondition struct { + // Type of strategy condition, Complete or Failed. + Type ServicePolicyConditionType + + // Status of the condition, one of True, False, Unknown + Status apiextensions.ConditionStatus + + // Last time the condition was checked. + // +optional + LastProbeTime metav1.Time + + // Last time the condition transit from one status to another + // +optional + LastTransitionTime metav1.Time + + // reason for the condition's last transition + Reason string + + // Human readable message indicating details about last transition. + // +optinal + Message string +} + +// ServicePolicyStatus defines the observed state of ServicePolicy +type ServicePolicyStatus struct { + // The latest available observations of an object's current state. + // +optional + Conditions []ServicePolicyCondition + + // Represents time when the strategy was acknowledged by the controller. + // It is represented in RFC3339 form and is in UTC. + // +optional + StartTime *metav1.Time + + // Represents time when the strategy was completed. + // It is represented in RFC3339 form and is in UTC. + // +optional + CompletionTime *metav1.Time +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ServicePolicy is the Schema for the servicepolicies API +// +k8s:openapi-gen=true +type ServicePolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServicePolicySpec `json:"spec,omitempty"` + Status ServicePolicyStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ServicePolicyList contains a list of ServicePolicy +type ServicePolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServicePolicy `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServicePolicy{}, &ServicePolicyList{}) +} diff --git a/pkg/apis/servicemesh/v1alpha2/servicepolicy_types_test.go b/pkg/apis/servicemesh/v1alpha2/servicepolicy_types_test.go new file mode 100644 index 000000000..4b48bc899 --- /dev/null +++ b/pkg/apis/servicemesh/v1alpha2/servicepolicy_types_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2019 The KubeSphere authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "testing" + + "github.com/onsi/gomega" + "golang.org/x/net/context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestStorageServicePolicy(t *testing.T) { + key := types.NamespacedName{ + Name: "foo", + Namespace: "default", + } + created := &ServicePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }} + g := gomega.NewGomegaWithT(t) + + // Test Create + fetched := &ServicePolicy{} + g.Expect(c.Create(context.TODO(), created)).NotTo(gomega.HaveOccurred()) + + g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred()) + g.Expect(fetched).To(gomega.Equal(created)) + + // Test Updating the Labels + updated := fetched.DeepCopy() + updated.Labels = map[string]string{"hello": "world"} + g.Expect(c.Update(context.TODO(), updated)).NotTo(gomega.HaveOccurred()) + + g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred()) + g.Expect(fetched).To(gomega.Equal(updated)) + + // Test Delete + g.Expect(c.Delete(context.TODO(), fetched)).NotTo(gomega.HaveOccurred()) + g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.HaveOccurred()) +} diff --git a/pkg/apis/servicemesh/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/servicemesh/v1alpha2/zz_generated.deepcopy.go index 02ce246fe..f16f7ebb5 100644 --- a/pkg/apis/servicemesh/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/servicemesh/v1alpha2/zz_generated.deepcopy.go @@ -25,6 +25,156 @@ 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 *DestinationRuleSpecTemplate) DeepCopyInto(out *DestinationRuleSpecTemplate) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DestinationRuleSpecTemplate. +func (in *DestinationRuleSpecTemplate) DeepCopy() *DestinationRuleSpecTemplate { + if in == nil { + return nil + } + out := new(DestinationRuleSpecTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServicePolicy) DeepCopyInto(out *ServicePolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePolicy. +func (in *ServicePolicy) DeepCopy() *ServicePolicy { + if in == nil { + return nil + } + out := new(ServicePolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServicePolicy) 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 *ServicePolicyCondition) DeepCopyInto(out *ServicePolicyCondition) { + *out = *in + in.LastProbeTime.DeepCopyInto(&out.LastProbeTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePolicyCondition. +func (in *ServicePolicyCondition) DeepCopy() *ServicePolicyCondition { + if in == nil { + return nil + } + out := new(ServicePolicyCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServicePolicyList) DeepCopyInto(out *ServicePolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServicePolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePolicyList. +func (in *ServicePolicyList) DeepCopy() *ServicePolicyList { + if in == nil { + return nil + } + out := new(ServicePolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServicePolicyList) 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 *ServicePolicySpec) DeepCopyInto(out *ServicePolicySpec) { + *out = *in + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + in.Template.DeepCopyInto(&out.Template) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePolicySpec. +func (in *ServicePolicySpec) DeepCopy() *ServicePolicySpec { + if in == nil { + return nil + } + out := new(ServicePolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServicePolicyStatus) DeepCopyInto(out *ServicePolicyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]ServicePolicyCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.StartTime != nil { + in, out := &in.StartTime, &out.StartTime + *out = (*in).DeepCopy() + } + if in.CompletionTime != nil { + in, out := &in.CompletionTime, &out.CompletionTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePolicyStatus. +func (in *ServicePolicyStatus) DeepCopy() *ServicePolicyStatus { + if in == nil { + return nil + } + out := new(ServicePolicyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Strategy) DeepCopyInto(out *Strategy) { *out = *in diff --git a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake/fake_servicemesh_client.go b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake/fake_servicemesh_client.go index 85a33cebf..ec4457300 100644 --- a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake/fake_servicemesh_client.go +++ b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake/fake_servicemesh_client.go @@ -28,6 +28,10 @@ type FakeServicemeshV1alpha2 struct { *testing.Fake } +func (c *FakeServicemeshV1alpha2) ServicePolicies(namespace string) v1alpha2.ServicePolicyInterface { + return &FakeServicePolicies{c, namespace} +} + func (c *FakeServicemeshV1alpha2) Strategies(namespace string) v1alpha2.StrategyInterface { return &FakeStrategies{c, namespace} } diff --git a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake/fake_servicepolicy.go b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake/fake_servicepolicy.go new file mode 100644 index 000000000..8f87ef5d2 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake/fake_servicepolicy.go @@ -0,0 +1,140 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + 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" + v1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" +) + +// FakeServicePolicies implements ServicePolicyInterface +type FakeServicePolicies struct { + Fake *FakeServicemeshV1alpha2 + ns string +} + +var servicepoliciesResource = schema.GroupVersionResource{Group: "servicemesh.kubesphere.io", Version: "v1alpha2", Resource: "servicepolicies"} + +var servicepoliciesKind = schema.GroupVersionKind{Group: "servicemesh.kubesphere.io", Version: "v1alpha2", Kind: "ServicePolicy"} + +// Get takes name of the servicePolicy, and returns the corresponding servicePolicy object, and an error if there is any. +func (c *FakeServicePolicies) Get(name string, options v1.GetOptions) (result *v1alpha2.ServicePolicy, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(servicepoliciesResource, c.ns, name), &v1alpha2.ServicePolicy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ServicePolicy), err +} + +// List takes label and field selectors, and returns the list of ServicePolicies that match those selectors. +func (c *FakeServicePolicies) List(opts v1.ListOptions) (result *v1alpha2.ServicePolicyList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(servicepoliciesResource, servicepoliciesKind, c.ns, opts), &v1alpha2.ServicePolicyList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.ServicePolicyList{ListMeta: obj.(*v1alpha2.ServicePolicyList).ListMeta} + for _, item := range obj.(*v1alpha2.ServicePolicyList).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 servicePolicies. +func (c *FakeServicePolicies) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(servicepoliciesResource, c.ns, opts)) + +} + +// Create takes the representation of a servicePolicy and creates it. Returns the server's representation of the servicePolicy, and an error, if there is any. +func (c *FakeServicePolicies) Create(servicePolicy *v1alpha2.ServicePolicy) (result *v1alpha2.ServicePolicy, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(servicepoliciesResource, c.ns, servicePolicy), &v1alpha2.ServicePolicy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ServicePolicy), err +} + +// Update takes the representation of a servicePolicy and updates it. Returns the server's representation of the servicePolicy, and an error, if there is any. +func (c *FakeServicePolicies) Update(servicePolicy *v1alpha2.ServicePolicy) (result *v1alpha2.ServicePolicy, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(servicepoliciesResource, c.ns, servicePolicy), &v1alpha2.ServicePolicy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ServicePolicy), 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 *FakeServicePolicies) UpdateStatus(servicePolicy *v1alpha2.ServicePolicy) (*v1alpha2.ServicePolicy, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(servicepoliciesResource, "status", c.ns, servicePolicy), &v1alpha2.ServicePolicy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ServicePolicy), err +} + +// Delete takes name of the servicePolicy and deletes it. Returns an error if one occurs. +func (c *FakeServicePolicies) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(servicepoliciesResource, c.ns, name), &v1alpha2.ServicePolicy{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeServicePolicies) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(servicepoliciesResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha2.ServicePolicyList{}) + return err +} + +// Patch applies the patch and returns the patched servicePolicy. +func (c *FakeServicePolicies) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.ServicePolicy, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(servicepoliciesResource, c.ns, name, pt, data, subresources...), &v1alpha2.ServicePolicy{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ServicePolicy), err +} diff --git a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/generated_expansion.go b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/generated_expansion.go index 32f9341fc..6a004902e 100644 --- a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/generated_expansion.go @@ -18,4 +18,6 @@ limitations under the License. package v1alpha2 +type ServicePolicyExpansion interface{} + type StrategyExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/servicemesh_client.go b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/servicemesh_client.go index 19d5bb18b..706f88184 100644 --- a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/servicemesh_client.go +++ b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/servicemesh_client.go @@ -27,6 +27,7 @@ import ( type ServicemeshV1alpha2Interface interface { RESTClient() rest.Interface + ServicePoliciesGetter StrategiesGetter } @@ -35,6 +36,10 @@ type ServicemeshV1alpha2Client struct { restClient rest.Interface } +func (c *ServicemeshV1alpha2Client) ServicePolicies(namespace string) ServicePolicyInterface { + return newServicePolicies(c, namespace) +} + func (c *ServicemeshV1alpha2Client) Strategies(namespace string) StrategyInterface { return newStrategies(c, namespace) } diff --git a/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/servicepolicy.go b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/servicepolicy.go new file mode 100644 index 000000000..fcf600c82 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/servicepolicy.go @@ -0,0 +1,191 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "time" + + 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" + v1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" + scheme "kubesphere.io/kubesphere/pkg/client/clientset/versioned/scheme" +) + +// ServicePoliciesGetter has a method to return a ServicePolicyInterface. +// A group's client should implement this interface. +type ServicePoliciesGetter interface { + ServicePolicies(namespace string) ServicePolicyInterface +} + +// ServicePolicyInterface has methods to work with ServicePolicy resources. +type ServicePolicyInterface interface { + Create(*v1alpha2.ServicePolicy) (*v1alpha2.ServicePolicy, error) + Update(*v1alpha2.ServicePolicy) (*v1alpha2.ServicePolicy, error) + UpdateStatus(*v1alpha2.ServicePolicy) (*v1alpha2.ServicePolicy, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha2.ServicePolicy, error) + List(opts v1.ListOptions) (*v1alpha2.ServicePolicyList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.ServicePolicy, err error) + ServicePolicyExpansion +} + +// servicePolicies implements ServicePolicyInterface +type servicePolicies struct { + client rest.Interface + ns string +} + +// newServicePolicies returns a ServicePolicies +func newServicePolicies(c *ServicemeshV1alpha2Client, namespace string) *servicePolicies { + return &servicePolicies{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the servicePolicy, and returns the corresponding servicePolicy object, and an error if there is any. +func (c *servicePolicies) Get(name string, options v1.GetOptions) (result *v1alpha2.ServicePolicy, err error) { + result = &v1alpha2.ServicePolicy{} + err = c.client.Get(). + Namespace(c.ns). + Resource("servicepolicies"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ServicePolicies that match those selectors. +func (c *servicePolicies) List(opts v1.ListOptions) (result *v1alpha2.ServicePolicyList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha2.ServicePolicyList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("servicepolicies"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested servicePolicies. +func (c *servicePolicies) Watch(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("servicepolicies"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch() +} + +// Create takes the representation of a servicePolicy and creates it. Returns the server's representation of the servicePolicy, and an error, if there is any. +func (c *servicePolicies) Create(servicePolicy *v1alpha2.ServicePolicy) (result *v1alpha2.ServicePolicy, err error) { + result = &v1alpha2.ServicePolicy{} + err = c.client.Post(). + Namespace(c.ns). + Resource("servicepolicies"). + Body(servicePolicy). + Do(). + Into(result) + return +} + +// Update takes the representation of a servicePolicy and updates it. Returns the server's representation of the servicePolicy, and an error, if there is any. +func (c *servicePolicies) Update(servicePolicy *v1alpha2.ServicePolicy) (result *v1alpha2.ServicePolicy, err error) { + result = &v1alpha2.ServicePolicy{} + err = c.client.Put(). + Namespace(c.ns). + Resource("servicepolicies"). + Name(servicePolicy.Name). + Body(servicePolicy). + Do(). + 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 *servicePolicies) UpdateStatus(servicePolicy *v1alpha2.ServicePolicy) (result *v1alpha2.ServicePolicy, err error) { + result = &v1alpha2.ServicePolicy{} + err = c.client.Put(). + Namespace(c.ns). + Resource("servicepolicies"). + Name(servicePolicy.Name). + SubResource("status"). + Body(servicePolicy). + Do(). + Into(result) + return +} + +// Delete takes name of the servicePolicy and deletes it. Returns an error if one occurs. +func (c *servicePolicies) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("servicepolicies"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *servicePolicies) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + var timeout time.Duration + if listOptions.TimeoutSeconds != nil { + timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("servicepolicies"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched servicePolicy. +func (c *servicePolicies) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.ServicePolicy, err error) { + result = &v1alpha2.ServicePolicy{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("servicepolicies"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index ec21cbfdf..effae9291 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -53,6 +53,8 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=servicemesh.kubesphere.io, Version=v1alpha2 + case v1alpha2.SchemeGroupVersion.WithResource("servicepolicies"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Servicemesh().V1alpha2().ServicePolicies().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("strategies"): return &genericInformer{resource: resource.GroupResource(), informer: f.Servicemesh().V1alpha2().Strategies().Informer()}, nil diff --git a/pkg/client/informers/externalversions/servicemesh/v1alpha2/interface.go b/pkg/client/informers/externalversions/servicemesh/v1alpha2/interface.go index 61a9c3d82..b36a77d9b 100644 --- a/pkg/client/informers/externalversions/servicemesh/v1alpha2/interface.go +++ b/pkg/client/informers/externalversions/servicemesh/v1alpha2/interface.go @@ -24,6 +24,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // ServicePolicies returns a ServicePolicyInformer. + ServicePolicies() ServicePolicyInformer // Strategies returns a StrategyInformer. Strategies() StrategyInformer } @@ -39,6 +41,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// ServicePolicies returns a ServicePolicyInformer. +func (v *version) ServicePolicies() ServicePolicyInformer { + return &servicePolicyInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Strategies returns a StrategyInformer. func (v *version) Strategies() StrategyInformer { return &strategyInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/informers/externalversions/servicemesh/v1alpha2/servicepolicy.go b/pkg/client/informers/externalversions/servicemesh/v1alpha2/servicepolicy.go new file mode 100644 index 000000000..5da94163a --- /dev/null +++ b/pkg/client/informers/externalversions/servicemesh/v1alpha2/servicepolicy.go @@ -0,0 +1,89 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" + servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" + versioned "kubesphere.io/kubesphere/pkg/client/clientset/versioned" + internalinterfaces "kubesphere.io/kubesphere/pkg/client/informers/externalversions/internalinterfaces" + v1alpha2 "kubesphere.io/kubesphere/pkg/client/listers/servicemesh/v1alpha2" +) + +// ServicePolicyInformer provides access to a shared informer and lister for +// ServicePolicies. +type ServicePolicyInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.ServicePolicyLister +} + +type servicePolicyInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewServicePolicyInformer constructs a new informer for ServicePolicy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewServicePolicyInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredServicePolicyInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredServicePolicyInformer constructs a new informer for ServicePolicy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredServicePolicyInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ServicemeshV1alpha2().ServicePolicies(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ServicemeshV1alpha2().ServicePolicies(namespace).Watch(options) + }, + }, + &servicemeshv1alpha2.ServicePolicy{}, + resyncPeriod, + indexers, + ) +} + +func (f *servicePolicyInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredServicePolicyInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *servicePolicyInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&servicemeshv1alpha2.ServicePolicy{}, f.defaultInformer) +} + +func (f *servicePolicyInformer) Lister() v1alpha2.ServicePolicyLister { + return v1alpha2.NewServicePolicyLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/listers/servicemesh/v1alpha2/expansion_generated.go b/pkg/client/listers/servicemesh/v1alpha2/expansion_generated.go index 1d68ccc64..3583a02d2 100644 --- a/pkg/client/listers/servicemesh/v1alpha2/expansion_generated.go +++ b/pkg/client/listers/servicemesh/v1alpha2/expansion_generated.go @@ -18,6 +18,14 @@ limitations under the License. package v1alpha2 +// ServicePolicyListerExpansion allows custom methods to be added to +// ServicePolicyLister. +type ServicePolicyListerExpansion interface{} + +// ServicePolicyNamespaceListerExpansion allows custom methods to be added to +// ServicePolicyNamespaceLister. +type ServicePolicyNamespaceListerExpansion interface{} + // StrategyListerExpansion allows custom methods to be added to // StrategyLister. type StrategyListerExpansion interface{} diff --git a/pkg/client/listers/servicemesh/v1alpha2/servicepolicy.go b/pkg/client/listers/servicemesh/v1alpha2/servicepolicy.go new file mode 100644 index 000000000..e46c41e58 --- /dev/null +++ b/pkg/client/listers/servicemesh/v1alpha2/servicepolicy.go @@ -0,0 +1,94 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" + v1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" +) + +// ServicePolicyLister helps list ServicePolicies. +type ServicePolicyLister interface { + // List lists all ServicePolicies in the indexer. + List(selector labels.Selector) (ret []*v1alpha2.ServicePolicy, err error) + // ServicePolicies returns an object that can list and get ServicePolicies. + ServicePolicies(namespace string) ServicePolicyNamespaceLister + ServicePolicyListerExpansion +} + +// servicePolicyLister implements the ServicePolicyLister interface. +type servicePolicyLister struct { + indexer cache.Indexer +} + +// NewServicePolicyLister returns a new ServicePolicyLister. +func NewServicePolicyLister(indexer cache.Indexer) ServicePolicyLister { + return &servicePolicyLister{indexer: indexer} +} + +// List lists all ServicePolicies in the indexer. +func (s *servicePolicyLister) List(selector labels.Selector) (ret []*v1alpha2.ServicePolicy, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.ServicePolicy)) + }) + return ret, err +} + +// ServicePolicies returns an object that can list and get ServicePolicies. +func (s *servicePolicyLister) ServicePolicies(namespace string) ServicePolicyNamespaceLister { + return servicePolicyNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ServicePolicyNamespaceLister helps list and get ServicePolicies. +type ServicePolicyNamespaceLister interface { + // List lists all ServicePolicies in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha2.ServicePolicy, err error) + // Get retrieves the ServicePolicy from the indexer for a given namespace and name. + Get(name string) (*v1alpha2.ServicePolicy, error) + ServicePolicyNamespaceListerExpansion +} + +// servicePolicyNamespaceLister implements the ServicePolicyNamespaceLister +// interface. +type servicePolicyNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ServicePolicies in the indexer for a given namespace. +func (s servicePolicyNamespaceLister) List(selector labels.Selector) (ret []*v1alpha2.ServicePolicy, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.ServicePolicy)) + }) + return ret, err +} + +// Get retrieves the ServicePolicy from the indexer for a given namespace and name. +func (s servicePolicyNamespaceLister) Get(name string) (*v1alpha2.ServicePolicy, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("servicepolicy"), name) + } + return obj.(*v1alpha2.ServicePolicy), nil +} diff --git a/pkg/controller/add_strategy.go b/pkg/controller/add_strategy.go index ff7f88282..c45c78749 100644 --- a/pkg/controller/add_strategy.go +++ b/pkg/controller/add_strategy.go @@ -17,13 +17,12 @@ limitations under the License. package controller import ( - "kubesphere.io/kubesphere/pkg/controller/strategy" "sigs.k8s.io/application/pkg/controller/application" ) func init() { // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. - AddToManagerFuncs = append(AddToManagerFuncs, strategy.Add) + //AddToManagerFuncs = append(AddToManagerFuncs, strategy.Add) // Add application to manager functions AddToManagerFuncs = append(AddToManagerFuncs, application.Add) diff --git a/pkg/controller/destinationrule/destinationrule_controller.go b/pkg/controller/destinationrule/destinationrule_controller.go index d914eaac3..90adce819 100644 --- a/pkg/controller/destinationrule/destinationrule_controller.go +++ b/pkg/controller/destinationrule/destinationrule_controller.go @@ -16,6 +16,7 @@ import ( v1core "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/util/metrics" + servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" "kubesphere.io/kubesphere/pkg/controller/virtualservice/util" "reflect" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" @@ -32,6 +33,9 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" "time" + + servicemeshinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/servicemesh/v1alpha2" + servicemeshlisters "kubesphere.io/kubesphere/pkg/client/listers/servicemesh/v1alpha2" ) const ( @@ -59,6 +63,9 @@ type DestinationRuleController struct { deploymentLister listersv1.DeploymentLister deploymentSynced cache.InformerSynced + servicePolicyLister servicemeshlisters.ServicePolicyLister + servicePolicySynced cache.InformerSynced + destinationRuleLister istiolisters.DestinationRuleLister destinationRuleSynced cache.InformerSynced @@ -70,6 +77,7 @@ type DestinationRuleController struct { func NewDestinationRuleController(deploymentInformer informersv1.DeploymentInformer, destinationRuleInformer istioinformers.DestinationRuleInformer, serviceInformer coreinformers.ServiceInformer, + servicePolicyInformer servicemeshinformers.ServicePolicyInformer, client clientset.Interface, destinationRuleClient istioclientset.Interface) *DestinationRuleController { @@ -116,6 +124,17 @@ func NewDestinationRuleController(deploymentInformer informersv1.DeploymentInfor v.destinationRuleLister = destinationRuleInformer.Lister() v.destinationRuleSynced = destinationRuleInformer.Informer().HasSynced + v.servicePolicyLister = servicePolicyInformer.Lister() + v.servicePolicySynced = servicePolicyInformer.Informer().HasSynced + + servicePolicyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: v.addServicePolicy, + UpdateFunc: func(old, cur interface{}) { + v.addServicePolicy(cur) + }, + DeleteFunc: v.addServicePolicy, + }) + v.eventBroadcaster = broadcaster v.eventRecorder = recorder @@ -136,7 +155,7 @@ func (v *DestinationRuleController) Run(workers int, stopCh <-chan struct{}) { log.Info("starting destinationrule controller") defer log.Info("shutting down destinationrule controller") - if !controller.WaitForCacheSync("destinationrule-controller", stopCh, v.serviceSynced, v.destinationRuleSynced, v.deploymentSynced) { + if !controller.WaitForCacheSync("destinationrule-controller", stopCh, v.serviceSynced, v.destinationRuleSynced, v.deploymentSynced, v.servicePolicySynced) { return } @@ -177,6 +196,8 @@ func (v *DestinationRuleController) processNextWorkItem() bool { return true } +// main function of the reconcile for destinationrule +// destinationrule's name is same with the service that created it func (v *DestinationRuleController) syncService(key string) error { startTime := time.Now() defer func() { @@ -192,14 +213,14 @@ func (v *DestinationRuleController) syncService(key string) error { if err != nil { // Delete the corresponding destinationrule, as the service has been deleted. err = v.destinationRuleClient.NetworkingV1alpha3().DestinationRules(namespace).Delete(name, nil) - if err != nil && !errors.IsNotFound(err) { + if !errors.IsNotFound(err) { + log.Error(err, "delete destination rule failed", "namespace", namespace, "name", name) return err } - return nil } - if len(service.Labels) < len(util.ApplicationLabels) || !util.IsApplicationComponent(&service.ObjectMeta) || + if len(service.Labels) < len(util.ApplicationLabels) || !util.IsApplicationComponent(service.Labels) || len(service.Spec.Ports) == 0 { // services don't have enough labels to create a virtualservice // or they don't have necessary labels @@ -207,14 +228,22 @@ func (v *DestinationRuleController) syncService(key string) error { return nil } + appName := util.GetComponentName(&service.ObjectMeta) + + // fetch all deployments that match with service selector deployments, err := v.deploymentLister.Deployments(namespace).List(labels.Set(service.Spec.Selector).AsSelectorPreValidated()) if err != nil { return err } - subsets := []v1alpha3.Subset{} + subsets := make([]v1alpha3.Subset, 0) for _, deployment := range deployments { + // not a valid deployment we required + if !util.IsApplicationComponent(deployment.Labels) || !util.IsApplicationComponent(deployment.Spec.Selector.MatchLabels) { + continue + } + version := util.GetComponentVersion(&deployment.ObjectMeta) if len(version) == 0 { @@ -248,19 +277,49 @@ func (v *DestinationRuleController) syncService(key string) error { log.Error(err, "Couldn't get destinationrule for service", "key", key) return err } + } + // fetch all servicepolicies associated to this service + servicePolicies, err := v.servicePolicyLister.ServicePolicies(namespace).List(labels.SelectorFromSet(map[string]string{util.AppLabel: appName})) + if err != nil { + log.Error(err, "could not list service policies is namespace with component name", "namespace", namespace, "name", appName) + return err + } + + dr := currentDestinationRule.DeepCopy() + dr.Spec.Subsets = subsets + // + if len(servicePolicies) > 0 { + if len(servicePolicies) > 1 { + err = fmt.Errorf("more than one service policy associated with service %s/%s is forbidden", namespace, name) + log.Error(err, "") + return err + } + + sp := servicePolicies[0] + if sp.Spec.Template.Spec.TrafficPolicy != nil { + dr.Spec.TrafficPolicy = sp.Spec.Template.Spec.TrafficPolicy + } + + for _, subset := range sp.Spec.Template.Spec.Subsets { + for i := range dr.Spec.Subsets { + if subset.Name == dr.Spec.Subsets[i].Name && subset.TrafficPolicy != nil { + dr.Spec.Subsets[i].TrafficPolicy = subset.TrafficPolicy + } + } + } } createDestinationRule := len(currentDestinationRule.ResourceVersion) == 0 - if !createDestinationRule && reflect.DeepEqual(currentDestinationRule.Spec.Subsets, subsets) && + if !createDestinationRule && reflect.DeepEqual(currentDestinationRule.Spec, dr.Spec) && reflect.DeepEqual(currentDestinationRule.Labels, service.Labels) { log.V(5).Info("destinationrule are equal, skipping update", "key", types.NamespacedName{Namespace: service.Namespace, Name: service.Name}.String()) return nil } newDestinationRule := currentDestinationRule.DeepCopy() - newDestinationRule.Spec.Subsets = subsets + newDestinationRule.Spec = dr.Spec newDestinationRule.Labels = service.Labels if newDestinationRule.Annotations == nil { newDestinationRule.Annotations = make(map[string]string) @@ -293,20 +352,13 @@ func (v *DestinationRuleController) syncService(key string) error { return nil } -func (v *DestinationRuleController) isApplicationComponent(meta *metav1.ObjectMeta) bool { - if len(meta.Labels) >= len(util.ApplicationLabels) && util.IsApplicationComponent(meta) { - return true - } - return false -} - // When a destinationrule is added, figure out which service it will be used // and enqueue it. obj must have *appsv1.Deployment type func (v *DestinationRuleController) addDeployment(obj interface{}) { deploy := obj.(*appsv1.Deployment) // not a application component - if !v.isApplicationComponent(&deploy.ObjectMeta) { + if !util.IsApplicationComponent(deploy.Labels) || !util.IsApplicationComponent(deploy.Spec.Selector.MatchLabels) { return } @@ -354,7 +406,7 @@ func (v *DestinationRuleController) getDeploymentServiceMemberShip(deployment *a for i := range allServices { service := allServices[i] - if service.Spec.Selector == nil || !v.isApplicationComponent(&service.ObjectMeta) { + if service.Spec.Selector == nil || !util.IsApplicationComponent(service.Labels) { // services with nil selectors match nothing, not everything. continue } @@ -371,6 +423,34 @@ func (v *DestinationRuleController) getDeploymentServiceMemberShip(deployment *a return set, nil } +func (v *DestinationRuleController) addServicePolicy(obj interface{}) { + servicePolicy := obj.(*servicemeshv1alpha2.ServicePolicy) + + appName := servicePolicy.Labels[util.AppLabel] + + services, err := v.serviceLister.Services(servicePolicy.Namespace).List(labels.SelectorFromSet(map[string]string{util.AppLabel: appName})) + if err != nil { + log.Error(err, "cannot list services", "namespace", servicePolicy.Namespace, "name", appName) + utilruntime.HandleError(fmt.Errorf("cannot list services in namespace %s, with component name %v", servicePolicy.Namespace, appName)) + return + } + + set := sets.String{} + for _, service := range services { + key, err := controller.KeyFunc(service) + if err != nil { + utilruntime.HandleError(err) + continue + } + set.Insert(key) + } + + // avoid enqueue a key multiple times + for key := range set { + v.queue.Add(key) + } +} + func (v *DestinationRuleController) handleErr(err error, key interface{}) { if err != nil { v.queue.Forget(key) @@ -383,7 +463,7 @@ func (v *DestinationRuleController) handleErr(err error, key interface{}) { return } - log.V(0).Info("Dropping service out of the queue", "key", key, "error", err) + log.V(4).Info("Dropping service out of the queue", "key", key, "error", err) v.queue.Forget(key) utilruntime.HandleError(err) } diff --git a/pkg/controller/destinationrule/destinationrule_controller_test.go b/pkg/controller/destinationrule/destinationrule_controller_test.go index 8aae18746..fead6c0c4 100644 --- a/pkg/controller/destinationrule/destinationrule_controller_test.go +++ b/pkg/controller/destinationrule/destinationrule_controller_test.go @@ -1 +1,58 @@ package destinationrule + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TODO(jeff): add test cases + +var namespace = "default" +var lbs = map[string]string{ + "app.kubernetes.io/name": "bookinfo", + "servicemesh.kubesphere.io/enabled": "", + "app": "reviews", +} + +var service = corev1.Service{} + +var deployments = []appsv1.Deployment{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "deploy-v1", + Labels: map[string]string{ + "app.kubernetes.io/name": "bookinfo", + "servicemesh.kubesphere.io/enabled": "", + "app": "reviews", + "version": "v1", + }, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app.kubernetes.io/name": "bookinfo", + "servicemesh.kubesphere.io/enabled": "", + "app": "reviews", + "version": "v1", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/name": "bookinfo", + "servicemesh.kubesphere.io/enabled": "", + "app": "reviews", + "version": "v1", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {}, + }, + }, + }, + }, + }, +} diff --git a/pkg/controller/strategy/helper.go b/pkg/controller/strategy/helper.go deleted file mode 100644 index a8b925b59..000000000 --- a/pkg/controller/strategy/helper.go +++ /dev/null @@ -1,51 +0,0 @@ -package strategy - -import ( - "fmt" - "github.com/knative/pkg/apis/istio/v1alpha3" - "k8s.io/api/core/v1" - "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" -) - -const ( - AppLabel = "app" -) - -func getAppNameByStrategy(strategy *v1alpha2.Strategy) string { - if len(strategy.Labels) > 0 && len(strategy.Labels[AppLabel]) > 0 { - return strategy.Labels[AppLabel] - } - return "" -} - -// if virtualservice not specified with port number, then fill with service first port -func fillDestinationPort(vs *v1alpha3.VirtualService, service *v1.Service) error { - - if len(service.Spec.Ports) == 0 { - return fmt.Errorf("service %s/%s spec doesn't canotain any ports", service.Namespace, service.Name) - } - - // fill http port - for i := range vs.Spec.Http { - for j := range vs.Spec.Http[i].Route { - if vs.Spec.Http[i].Route[j].Destination.Port.Number == 0 { - vs.Spec.Http[i].Route[j].Destination.Port.Number = uint32(service.Spec.Ports[0].Port) - } - } - - if vs.Spec.Http[i].Mirror != nil && vs.Spec.Http[i].Mirror.Port.Number == 0 { - vs.Spec.Http[i].Mirror.Port.Number = uint32(service.Spec.Ports[0].Port) - } - } - - // fill tcp port - for i := range vs.Spec.Tcp { - for j := range vs.Spec.Tcp[i].Route { - if vs.Spec.Tcp[i].Route[j].Destination.Port.Number == 0 { - vs.Spec.Tcp[i].Route[j].Destination.Port.Number = uint32(service.Spec.Ports[0].Port) - } - } - } - - return nil -} diff --git a/pkg/controller/strategy/strategy_controller.go b/pkg/controller/strategy/strategy_controller.go deleted file mode 100644 index aa14ddb5d..000000000 --- a/pkg/controller/strategy/strategy_controller.go +++ /dev/null @@ -1,192 +0,0 @@ -/* -Copyright 2019 The KubeSphere authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package strategy - -import ( - "context" - "fmt" - "github.com/knative/pkg/apis/istio/v1alpha3" - "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" - "reflect" - "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" - logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" - "sigs.k8s.io/controller-runtime/pkg/source" -) - -var log = logf.Log.WithName("strategy-controller") - -// Add creates a new Strategy 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 &ReconcileStrategy{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("strategy-controller", mgr, controller.Options{Reconciler: r}) - if err != nil { - return err - } - - // Watch for changes to Strategy - err = c.Watch(&source.Kind{Type: &servicemeshv1alpha2.Strategy{}}, &handler.EnqueueRequestForObject{}) - if err != nil { - return err - } - - // TODO(user): Modify this to be the types you create - // Watch a VirtualService created by Strategy - err = c.Watch(&source.Kind{Type: &v1alpha3.VirtualService{}}, &handler.EnqueueRequestForOwner{ - IsController: true, - OwnerType: &servicemeshv1alpha2.Strategy{}, - }) - if err != nil { - return err - } - - return nil -} - -var _ reconcile.Reconciler = &ReconcileStrategy{} - -// ReconcileStrategy reconciles a Strategy object -type ReconcileStrategy struct { - client.Client - scheme *runtime.Scheme -} - -// Reconcile reads that state of the cluster for a Strategy object and makes changes based on the state read -// and what is in the Strategy.Spec -// a Deployment as an example -// Automatically generate RBAC rules to allow the Controller to read and write Deployments -// +kubebuilder:rbac:groups=networking.istio.io,resources=virtualservices,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=networking.istio.io,resources=virtualservices/status,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=servicemesh.kubesphere.io,resources=strategies,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=servicemesh.kubesphere.io,resources=strategies/status,verbs=get;update;patch -func (r *ReconcileStrategy) Reconcile(request reconcile.Request) (reconcile.Result, error) { - - // Fetch the Strategy instance - strategy := &servicemeshv1alpha2.Strategy{} - err := r.Get(context.TODO(), request.NamespacedName, strategy) - - if err != nil { - if errors.IsNotFound(err) { - return reconcile.Result{}, nil - } - return reconcile.Result{}, err - } - - return r.reconcileStrategy(strategy) -} - -func (r *ReconcileStrategy) reconcileStrategy(strategy *servicemeshv1alpha2.Strategy) (reconcile.Result, error) { - - appName := getAppNameByStrategy(strategy) - service := &v1.Service{} - - err := r.Get(context.TODO(), types.NamespacedName{Namespace: strategy.Namespace, Name: appName}, service) - if err != nil { - log.Error(err, "couldn't find service", "namespace", strategy.Namespace, "name", appName) - return reconcile.Result{}, errors.NewBadRequest(fmt.Sprintf("service %s not found", appName)) - } - - vs, err := r.generateVirtualService(strategy, service) - - // Check if the VirtualService already exists - found := &v1alpha3.VirtualService{} - err = r.Get(context.TODO(), types.NamespacedName{Name: vs.Name, Namespace: vs.Namespace}, found) - if err != nil && errors.IsNotFound(err) { - log.Info("Creating VirtualService", "namespace", vs.Namespace, "name", vs.Name) - err = r.Create(context.TODO(), vs) - - return reconcile.Result{}, err - } else if err != nil { - return reconcile.Result{}, err - } - - // Update the found object and write the result back if there are any changes - if !reflect.DeepEqual(vs.Spec, found.Spec) || len(vs.OwnerReferences) == 0 { - found.Spec = vs.Spec - found.OwnerReferences = vs.OwnerReferences - log.Info("Updating VirtualService", "namespace", vs.Namespace, "name", vs.Name) - err = r.Update(context.TODO(), found) - if err != nil { - return reconcile.Result{}, err - } - } - return reconcile.Result{}, nil -} - -func (r *ReconcileStrategy) generateVirtualService(strategy *servicemeshv1alpha2.Strategy, service *v1.Service) (*v1alpha3.VirtualService, error) { - - // Define VirtualService to be created - vs := &v1alpha3.VirtualService{ - ObjectMeta: metav1.ObjectMeta{ - Name: getAppNameByStrategy(strategy), - Namespace: strategy.Namespace, - Labels: strategy.Spec.Selector.MatchLabels, - }, - Spec: strategy.Spec.Template.Spec, - } - - // one version rules them all - if len(strategy.Spec.GovernorVersion) > 0 { - - governorDestinationWeight := v1alpha3.DestinationWeight{ - Destination: v1alpha3.Destination{ - Host: getAppNameByStrategy(strategy), - Subset: strategy.Spec.GovernorVersion, - }, - Weight: 100, - } - - if len(strategy.Spec.Template.Spec.Http) > 0 { - governorRoute := v1alpha3.HTTPRoute{ - Route: []v1alpha3.DestinationWeight{governorDestinationWeight}, - } - - vs.Spec.Http = []v1alpha3.HTTPRoute{governorRoute} - } else if len(strategy.Spec.Template.Spec.Tcp) > 0 { - governorRoute := v1alpha3.TCPRoute{ - Route: []v1alpha3.DestinationWeight{governorDestinationWeight}, - } - vs.Spec.Tcp = []v1alpha3.TCPRoute{governorRoute} - } - - } - - if err := fillDestinationPort(vs, service); err != nil { - return nil, err - } - - return vs, nil -} diff --git a/pkg/controller/strategy/strategy_controller_suite_test.go b/pkg/controller/strategy/strategy_controller_suite_test.go deleted file mode 100644 index 546e3b9a0..000000000 --- a/pkg/controller/strategy/strategy_controller_suite_test.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2019 The KubeSphere authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package strategy - -import ( - stdlog "log" - "os" - "path/filepath" - "sync" - "testing" - - "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "kubesphere.io/kubesphere/pkg/apis" - "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -var cfg *rest.Config - -func TestMain(m *testing.M) { - t := &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crds")}, - } - apis.AddToScheme(scheme.Scheme) - - var err error - if cfg, err = t.Start(); err != nil { - stdlog.Fatal(err) - } - - code := m.Run() - t.Stop() - os.Exit(code) -} - -// SetupTestReconcile returns a reconcile.Reconcile implementation that delegates to inner and -// writes the request to requests after Reconcile is finished. -func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) { - requests := make(chan reconcile.Request) - fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { - result, err := inner.Reconcile(req) - requests <- req - return result, err - }) - return fn, requests -} - -// StartTestManager adds recFn -func StartTestManager(mgr manager.Manager, g *gomega.GomegaWithT) (chan struct{}, *sync.WaitGroup) { - stop := make(chan struct{}) - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - g.Expect(mgr.Start(stop)).NotTo(gomega.HaveOccurred()) - }() - return stop, wg -} diff --git a/pkg/controller/strategy/strategy_controller_test.go b/pkg/controller/strategy/strategy_controller_test.go deleted file mode 100644 index f3cc5786a..000000000 --- a/pkg/controller/strategy/strategy_controller_test.go +++ /dev/null @@ -1,177 +0,0 @@ -/* -Copyright 2019 The KubeSphere authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package strategy - -import ( - "github.com/knative/pkg/apis/istio/common/v1alpha1" - "github.com/knative/pkg/apis/istio/v1alpha3" - "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/json" - "testing" - "time" - - "github.com/onsi/gomega" - "golang.org/x/net/context" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -var c client.Client - -var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: "foo", Namespace: "default"}} -var depKey = types.NamespacedName{Name: "details", Namespace: "default"} - -const timeout = time.Second * 5 - -var labels = map[string]string{ - "app.kubernetes.io/name": "details", - "app.kubernetes.io/version": "v1", - "app": "details", - "servicemesh.kubesphere.io/enabled": "", -} - -var svc = v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "details", - Namespace: "default", - Labels: labels, - }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Name: "http", - Port: 8080, - Protocol: v1.ProtocolTCP, - }, - }, - Selector: labels, - }, -} - -func TestReconcile(t *testing.T) { - g := gomega.NewGomegaWithT(t) - instance := &servicemeshv1alpha2.Strategy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - Namespace: "default", - Labels: labels, - }, - Spec: servicemeshv1alpha2.StrategySpec{ - Type: servicemeshv1alpha2.CanaryType, - Selector: &metav1.LabelSelector{ - MatchLabels: labels, - }, - Template: servicemeshv1alpha2.VirtualServiceTemplateSpec{ - Spec: v1alpha3.VirtualServiceSpec{ - Hosts: []string{"details"}, - Gateways: []string{"default"}, - Http: []v1alpha3.HTTPRoute{ - { - Match: []v1alpha3.HTTPMatchRequest{ - { - Method: &v1alpha1.StringMatch{ - Exact: "POST", - }, - }, - }, - Route: []v1alpha3.DestinationWeight{ - { - Destination: v1alpha3.Destination{ - Host: "details", - Subset: "v1", - }, - Weight: 60, - }, - }, - }, - { - Route: []v1alpha3.DestinationWeight{ - { - Destination: v1alpha3.Destination{ - Host: "details", - Subset: "v2", - }, - Weight: 40, - }, - }, - }, - }, - }, - }, - }, - } - - // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a - // channel when it is finished. - mgr, err := manager.New(cfg, manager.Options{}) - g.Expect(err).NotTo(gomega.HaveOccurred()) - c = mgr.GetClient() - - recFn, requests := SetupTestReconcile(newReconciler(mgr)) - g.Expect(add(mgr, recFn)).NotTo(gomega.HaveOccurred()) - - stopMgr, mgrStopped := StartTestManager(mgr, g) - - defer func() { - close(stopMgr) - mgrStopped.Wait() - }() - - err = c.Create(context.TODO(), &svc) - if apierrors.IsInvalid(err) { - t.Logf("failed to create service, %v", err) - return - } - g.Expect(err).NotTo(gomega.HaveOccurred()) - //defer c.Delete(context.TODO(), &svc) - - // Create the Strategy object and expect the Reconcile and Deployment to be created - err = c.Create(context.TODO(), instance) - // The instance object may not be a valid object because it might be missing some required fields. - // Please modify the instance object by adding required fields and then remove the following if statement. - if apierrors.IsInvalid(err) { - t.Logf("failed to create object, got an invalid object error: %v", err) - return - } - - g.Expect(err).NotTo(gomega.HaveOccurred()) - defer c.Delete(context.TODO(), instance) - g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedRequest))) - - vs := &v1alpha3.VirtualService{} - g.Eventually(func() error { return c.Get(context.TODO(), depKey, vs) }, timeout). - Should(gomega.Succeed()) - - if str, err := json.Marshal(vs); err == nil { - t.Logf("Created virtual service %s\n", str) - } - - // Delete the Deployment and expect Reconcile to be called for Deployment deletion - g.Expect(c.Delete(context.TODO(), vs)).NotTo(gomega.HaveOccurred()) - //g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedRequest))) - //g.Eventually(func() error { return c.Get(context.TODO(), depKey, vs) }, timeout).Should(gomega.Succeed()) - - // Manually delete Deployment since GC isn't enabled in the test control plane - g.Eventually(func() error { return c.Delete(context.TODO(), vs) }, timeout). - Should(gomega.MatchError("virtualservices.networking.istio.io \"details\" not found")) - -} diff --git a/pkg/controller/virtualservice/util/util.go b/pkg/controller/virtualservice/util/util.go index 75aa5f77a..007337c04 100644 --- a/pkg/controller/virtualservice/util/util.go +++ b/pkg/controller/virtualservice/util/util.go @@ -1,6 +1,8 @@ package util import ( + "github.com/knative/pkg/apis/istio/v1alpha3" + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "strings" ) @@ -47,9 +49,9 @@ func GetComponentVersion(meta *metav1.ObjectMeta) string { func ExtractApplicationLabels(meta *metav1.ObjectMeta) map[string]string { - labels := make(map[string]string, 0) + labels := make(map[string]string, len(ApplicationLabels)) for _, label := range ApplicationLabels { - if len(meta.Labels[label]) == 0 { + if _, ok := meta.Labels[label]; !ok { return nil } else { labels[label] = meta.Labels[label] @@ -59,13 +61,38 @@ func ExtractApplicationLabels(meta *metav1.ObjectMeta) map[string]string { return labels } -func IsApplicationComponent(meta *metav1.ObjectMeta) bool { +func IsApplicationComponent(lbs map[string]string) bool { for _, label := range ApplicationLabels { - if len(meta.Labels[label]) == 0 { + if _, ok := lbs[label]; !ok { return false } } return true } + +// if virtualservice not specified with port number, then fill with service first port +func FillDestinationPort(vs *v1alpha3.VirtualService, service *v1.Service) { + // fill http port + for i := range vs.Spec.Http { + for j := range vs.Spec.Http[i].Route { + if vs.Spec.Http[i].Route[j].Destination.Port.Number == 0 { + vs.Spec.Http[i].Route[j].Destination.Port.Number = uint32(service.Spec.Ports[0].Port) + } + } + + if vs.Spec.Http[i].Mirror != nil && vs.Spec.Http[i].Mirror.Port.Number == 0 { + vs.Spec.Http[i].Mirror.Port.Number = uint32(service.Spec.Ports[0].Port) + } + } + + // fill tcp port + for i := range vs.Spec.Tcp { + for j := range vs.Spec.Tcp[i].Route { + if vs.Spec.Tcp[i].Route[j].Destination.Port.Number == 0 { + vs.Spec.Tcp[i].Route[j].Destination.Port.Number = uint32(service.Spec.Ports[0].Port) + } + } + } +} diff --git a/pkg/controller/virtualservice/virtualservice_controller.go b/pkg/controller/virtualservice/virtualservice_controller.go index ca47b13ba..eee6fbb2b 100644 --- a/pkg/controller/virtualservice/virtualservice_controller.go +++ b/pkg/controller/virtualservice/virtualservice_controller.go @@ -6,14 +6,17 @@ import ( "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes/scheme" v1core "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/util/metrics" "kubesphere.io/kubesphere/pkg/controller/virtualservice/util" + "reflect" "strings" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" @@ -27,6 +30,7 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" + servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" servicemeshinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/servicemesh/v1alpha2" servicemeshlisters "kubesphere.io/kubesphere/pkg/client/listers/servicemesh/v1alpha2" @@ -98,6 +102,7 @@ func NewVirtualServiceController(serviceInformer coreinformers.ServiceInformer, AddFunc: v.enqueueService, DeleteFunc: v.enqueueService, UpdateFunc: func(old, cur interface{}) { + // TODO(jeff): need a more robust mechanism, because user may change labels v.enqueueService(cur) }, }) @@ -109,7 +114,11 @@ func NewVirtualServiceController(serviceInformer coreinformers.ServiceInformer, v.strategySynced = strategyInformer.Informer().HasSynced strategyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - DeleteFunc: v.deleteStrategy, + DeleteFunc: v.addStrategy, + AddFunc: v.addStrategy, + UpdateFunc: func(old, cur interface{}) { + v.addStrategy(cur) + }, }) v.destinationRuleLister = destinationRuleInformer.Lister() @@ -185,48 +194,63 @@ func (v *VirtualServiceController) processNextWorkItem() bool { return true } +// created virtualservice's name are same as the service name, same +// as the destinationrule name +// labels: +// servicemesh.kubernetes.io/enabled: "" +// app.kubernetes.io/name: bookinfo +// app: reviews +// are used to bind them together. +// syncService are the main part of reconcile function body, it takes +// service, destinationrule, strategy as input to create a virtualservice +// for service. func (v *VirtualServiceController) syncService(key string) error { startTime := time.Now() - defer func() { - log.V(4).Info("Finished syncing service virtualservice. ", "service", key, "duration", time.Since(startTime)) - }() - namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { + log.Error(err, "not a valid controller key", "key", key) return err } + // default component name to service name + appName := name + + defer func() { + log.V(4).Info("Finished syncing service virtualservice.", "namespace", namespace, "name", name, "duration", time.Since(startTime)) + }() + service, err := v.serviceLister.Services(namespace).Get(name) if err != nil { - // Delete the corresponding virtualservice, as the service has been deleted. - err = v.virtualServiceClient.NetworkingV1alpha3().VirtualServices(namespace).Delete(name, nil) - if err != nil && !errors.IsNotFound(err) { - log.Error(err, "delete orphan virtualservice failed", "namespace", service.Namespace, "name", service.Name) - return err + if errors.IsNotFound(err) { + // Delete the corresponding virtualservice, as the service has been deleted. + err = v.virtualServiceClient.NetworkingV1alpha3().VirtualServices(namespace).Delete(name, nil) + if err != nil && !errors.IsNotFound(err) { + log.Error(err, "delete orphan virtualservice failed", "namespace", namespace, "name", service.Name) + return err + } + return nil } - return nil + log.Error(err, "get service failed", "namespace", namespace, "name", name) + return err } - if len(service.Labels) < len(util.ApplicationLabels) || !util.IsApplicationComponent(&service.ObjectMeta) || + if len(service.Labels) < len(util.ApplicationLabels) || !util.IsApplicationComponent(service.Labels) || len(service.Spec.Ports) == 0 { // services don't have enough labels to create a virtualservice // or they don't have necessary labels // or they don't have any ports defined return nil } - - vs, err := v.virtualServiceLister.VirtualServices(namespace).Get(name) - if err == nil { - // there already is virtual service there, no need to create another one - return nil - } + // get real component name, i.e label app value + appName = util.GetComponentName(&service.ObjectMeta) destinationRule, err := v.destinationRuleLister.DestinationRules(namespace).Get(name) if err != nil { if errors.IsNotFound(err) { // there is no destinationrule for this service // maybe corresponding workloads are not created yet - return nil + log.Info("destination rules for service not found, retrying.", "namespace", namespace, "name", name) + return fmt.Errorf("destination rule for service %s/%s not found", namespace, name) } log.Error(err, "Couldn't get destinationrule for service.", "service", types.NamespacedName{Name: service.Name, Namespace: service.Namespace}.String()) return err @@ -235,20 +259,47 @@ func (v *VirtualServiceController) syncService(key string) error { subsets := destinationRule.Spec.Subsets if len(subsets) == 0 { // destination rule with no subsets, not possibly - err = fmt.Errorf("find destinationrule with no subsets for service %s", name) - log.Error(err, "Find destinationrule with no subsets for service", "namespace", service.Namespace, "name", name) + err = fmt.Errorf("found destinationrule with no subsets for service %s", name) + log.Error(err, "found destinationrule with no subsets", "namespace", namespace, "name", appName) return err - } else { - vs = &v1alpha3.VirtualService{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: util.ExtractApplicationLabels(&service.ObjectMeta), - }, - Spec: v1alpha3.VirtualServiceSpec{ - Hosts: []string{name}, - }, + } + + // fetch all strategies applied to service + strategies, err := v.strategyLister.Strategies(namespace).List(labels.SelectorFromSet(map[string]string{util.AppLabel: appName})) + if err != nil { + log.Error(err, "list strategies for service failed", "namespace", namespace, "name", appName) + return err + } else if len(strategies) > 1 { + // more than one strategies are not allowed, it will cause collision + err = fmt.Errorf("more than one strategies applied to service %s/%s is forbbiden", namespace, appName) + log.Error(err, "") + return err + } + + // get current virtual service + currentVirtualService, err := v.virtualServiceLister.VirtualServices(namespace).Get(appName) + if err != nil { + if errors.IsNotFound(err) { + currentVirtualService = &v1alpha3.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + Labels: util.ExtractApplicationLabels(&service.ObjectMeta), + }, + } } + return nil + } + vs := currentVirtualService.DeepCopy() + + if len(strategies) > 0 { + // apply strategy spec to virtualservice + vs.Spec = v.generateVirtualServiceSpec(strategies[0], service).Spec + } else { + // create a whole new virtualservice + + // TODO(jeff): use FQDN to replace service name + vs.Spec.Hosts = []string{name} // check if service has TCP protocol ports for _, port := range service.Spec.Ports { @@ -275,18 +326,45 @@ func (v *VirtualServiceController) syncService(key string) error { vs.Spec.Tcp = []v1alpha3.TCPRoute{{Route: []v1alpha3.DestinationWeight{route}}} } } + } - if len(vs.Spec.Http) > 0 || len(vs.Spec.Tcp) > 0 { - _, err := v.virtualServiceClient.NetworkingV1alpha3().VirtualServices(namespace).Create(vs) - if err != nil { - v.eventRecorder.Event(vs, v1.EventTypeWarning, "FailedToCreateVirtualService", fmt.Sprintf("Failed to create virtualservice for service %v/%v: %v", service.Namespace, service.Name, err)) - log.Error(err, "create virtualservice for service failed.", "service", service) - return err - } + createVirtualService := len(currentVirtualService.ResourceVersion) == 0 + + if !createVirtualService && + reflect.DeepEqual(vs.Spec, currentVirtualService.Spec) && + reflect.DeepEqual(service.Labels, currentVirtualService.Labels) { + log.V(4).Info("virtual service are equal, skipping update ") + return nil + } + + newVirtualService := currentVirtualService.DeepCopy() + newVirtualService.Labels = service.Labels + newVirtualService.Spec = vs.Spec + if newVirtualService.Annotations == nil { + newVirtualService.Annotations = make(map[string]string) + } + + if len(newVirtualService.Spec.Http) == 0 && len(newVirtualService.Spec.Tcp) == 0 && len(newVirtualService.Spec.Tls) == 0 { + err = fmt.Errorf("service %s/%s doesn't have a valid port spec", namespace, name) + log.Error(err, "") + return err + } + + if createVirtualService { + _, err = v.virtualServiceClient.NetworkingV1alpha3().VirtualServices(namespace).Create(newVirtualService) + } else { + _, err = v.virtualServiceClient.NetworkingV1alpha3().VirtualServices(namespace).Update(newVirtualService) + } + + if err != nil { + + if createVirtualService { + v.eventRecorder.Event(newVirtualService, v1.EventTypeWarning, "FailedToCreateVirtualService", fmt.Sprintf("Failed to create virtualservice for service %v/%v: %v", namespace, name, err)) } else { - log.Info("service doesn't have a tcp port.") - return nil + v.eventRecorder.Event(newVirtualService, v1.EventTypeWarning, "FailedToUpdateVirtualService", fmt.Sprintf("Failed to update virtualservice for service %v/%v: %v", namespace, name, err)) } + + return err } return nil @@ -299,7 +377,7 @@ func (v *VirtualServiceController) addDestinationRule(obj interface{}) { service, err := v.serviceLister.Services(dr.Namespace).Get(dr.Name) if err != nil { if errors.IsNotFound(err) { - log.V(0).Info("service not created yet", "namespace", dr.Namespace, "service", dr.Name) + log.V(3).Info("service not created yet", "namespace", dr.Namespace, "service", dr.Name) return } utilruntime.HandleError(fmt.Errorf("unable to get service with name %s/%s", dr.Namespace, dr.Name)) @@ -324,8 +402,46 @@ func (v *VirtualServiceController) addDestinationRule(obj interface{}) { return } -func (v *VirtualServiceController) deleteStrategy(obj interface{}) { - // nothing to do right now +// when a strategy created +func (v *VirtualServiceController) addStrategy(obj interface{}) { + strategy := obj.(*servicemeshv1alpha2.Strategy) + + lbs := util.ExtractApplicationLabels(&strategy.ObjectMeta) + if len(lbs) == 0 { + err := fmt.Errorf("invalid strategy %s/%s labels %s, not have required labels", strategy.Namespace, strategy.Name, strategy.Labels) + log.Error(err, "") + utilruntime.HandleError(err) + return + } + + allServices, err := v.serviceLister.Services(strategy.Namespace).List(labels.SelectorFromSet(lbs)) + if err != nil { + log.Error(err, "list services failed") + utilruntime.HandleError(err) + return + } + + // avoid insert a key multiple times + set := sets.String{} + + for i := range allServices { + service := allServices[i] + if service.Spec.Selector == nil || len(service.Spec.Ports) == 0 { + // services with nil selectors match nothing, not everything. + continue + } + + key, err := controller.KeyFunc(service) + if err != nil { + utilruntime.HandleError(err) + return + } + set.Insert(key) + } + + for key := range set { + v.queue.Add(key) + } } func (v *VirtualServiceController) handleErr(err error, key interface{}) { @@ -344,3 +460,40 @@ func (v *VirtualServiceController) handleErr(err error, key interface{}) { v.queue.Forget(key) utilruntime.HandleError(err) } + +func (v *VirtualServiceController) generateVirtualServiceSpec(strategy *servicemeshv1alpha2.Strategy, service *v1.Service) *v1alpha3.VirtualService { + + // Define VirtualService to be created + vs := &v1alpha3.VirtualService{ + Spec: strategy.Spec.Template.Spec, + } + + // one version rules them all + if len(strategy.Spec.GovernorVersion) > 0 { + + governorDestinationWeight := v1alpha3.DestinationWeight{ + Destination: v1alpha3.Destination{ + Host: service.Name, + Subset: strategy.Spec.GovernorVersion, + }, + Weight: 100, + } + + if len(strategy.Spec.Template.Spec.Http) > 0 { + governorRoute := v1alpha3.HTTPRoute{ + Route: []v1alpha3.DestinationWeight{governorDestinationWeight}, + } + + vs.Spec.Http = []v1alpha3.HTTPRoute{governorRoute} + } else if len(strategy.Spec.Template.Spec.Tcp) > 0 { + governorRoute := v1alpha3.TCPRoute{ + Route: []v1alpha3.DestinationWeight{governorDestinationWeight}, + } + vs.Spec.Tcp = []v1alpha3.TCPRoute{governorRoute} + } + + } + + util.FillDestinationPort(vs, service) + return vs +} diff --git a/pkg/simple/client/k8s/k8sclient.go b/pkg/simple/client/k8s/k8sclient.go index fb738e164..62f8a56da 100644 --- a/pkg/simple/client/k8s/k8sclient.go +++ b/pkg/simple/client/k8s/k8sclient.go @@ -34,12 +34,12 @@ var ( k8sClient *kubernetes.Clientset k8sClientOnce sync.Once KubeConfig *rest.Config - masterURL string + masterURL string ) func init() { flag.StringVar(&kubeConfigFile, "kubeconfig", "", "path to kubeconfig file") - flag.StringVar(&masterURL, "master-url","", "kube-apiserver url, only needed when out of cluster") + flag.StringVar(&masterURL, "master-url", "", "kube-apiserver url, only needed when out of cluster") } func Client() *kubernetes.Clientset {